@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.
- package/CHANGELOG.md +44 -0
- package/CLAUDE.md +13 -0
- package/binding.gyp +1 -0
- package/claude.sh +29 -5
- package/dist/index.cjs +237 -129
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +55 -3
- package/dist/index.d.mts +55 -3
- package/dist/index.d.ts +55 -3
- package/dist/index.mjs +236 -130
- package/dist/index.mjs.map +1 -1
- package/doc/SECURITY_AUDIT_2025.md +1 -1
- package/doc/SECURITY_AUDIT_2026.md +361 -0
- package/doc/TPP-GUIDE.md +144 -0
- package/doc/system-volume-detection.md +268 -0
- package/package.json +12 -12
- 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/src/binding.cpp +11 -0
- package/src/common/volume_metadata.h +10 -3
- package/src/common/volume_mount_points.h +7 -1
- package/src/darwin/da_mutex.h +23 -0
- package/src/darwin/get_mount_point.cpp +96 -0
- package/src/darwin/get_mount_point.h +13 -0
- package/src/darwin/raii_utils.h +39 -0
- package/src/darwin/system_volume.h +156 -0
- package/src/darwin/volume_metadata.cpp +18 -2
- package/src/darwin/volume_mount_points.cpp +46 -14
- package/src/index.ts +49 -0
- package/src/linux/mtab.ts +6 -0
- package/src/mount_point_for_path.ts +54 -0
- package/src/options.ts +7 -17
- package/src/path.ts +16 -1
- package/src/system_volume.ts +5 -9
- package/src/test-utils/assert.ts +4 -0
- package/src/types/mount_point.ts +28 -1
- package/src/types/native_bindings.ts +7 -0
- package/src/volume_metadata.ts +117 -2
- package/src/windows/system_volume.h +21 -16
- package/src/windows/volume_metadata.cpp +13 -7
- package/src/windows/volume_mount_points.cpp +11 -7
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
# System Volume Detection
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
`fs-metadata` uses a two-layer detection strategy across all platforms:
|
|
6
|
+
|
|
7
|
+
1. **Layer 1 (C++, native)** — platform-specific APIs detect system volumes at
|
|
8
|
+
enumeration time. Each `MountPoint` gets `isSystemVolume` set by native code.
|
|
9
|
+
2. **Layer 2 (TypeScript, heuristic)** — path patterns and filesystem type lists
|
|
10
|
+
catch pseudo-filesystems, container runtimes, and platform-specific system
|
|
11
|
+
paths. Heuristics **never downgrade** a native `true` — they only upgrade
|
|
12
|
+
`false` to `true`.
|
|
13
|
+
|
|
14
|
+
The `isSystemVolume` field is exposed on both `MountPoint` and `VolumeMetadata`.
|
|
15
|
+
|
|
16
|
+
By default, system volumes are **excluded** from `getAllVolumeMetadata()` results
|
|
17
|
+
on Linux and macOS (`includeSystemVolumes: false`), but **included** on Windows
|
|
18
|
+
because `C:\` is both a system drive and the primary user storage location.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## macOS
|
|
23
|
+
|
|
24
|
+
### Background
|
|
25
|
+
|
|
26
|
+
On macOS 10.15 (Catalina) and later, the boot volume is split into multiple
|
|
27
|
+
APFS volumes within a single container:
|
|
28
|
+
|
|
29
|
+
| Volume | Mount Point | APFS Role | Description |
|
|
30
|
+
| ------------------- | ---------------------------- | --------- | --------------------------------------------------------- |
|
|
31
|
+
| Macintosh HD | `/` (snapshot) | System | Sealed, read-only OS snapshot |
|
|
32
|
+
| Macintosh HD - Data | `/System/Volumes/Data` | Data | User data (firmlinked to `/Users`, `/Applications`, etc.) |
|
|
33
|
+
| VM | `/System/Volumes/VM` | VM | Virtual memory swap |
|
|
34
|
+
| Preboot | `/System/Volumes/Preboot` | Preboot | Boot-time resources |
|
|
35
|
+
| Recovery | `/System/Volumes/Recovery` | Recovery | Recovery OS |
|
|
36
|
+
| Update | `/System/Volumes/Update` | Update | OS update staging |
|
|
37
|
+
| Hardware | `/System/Volumes/Hardware` | Hardware | Hardware-specific data |
|
|
38
|
+
| xarts | `/System/Volumes/xarts` | xART | Secure token storage |
|
|
39
|
+
| iSCPreboot | `/System/Volumes/iSCPreboot` | Prelogin | Pre-login resources |
|
|
40
|
+
|
|
41
|
+
### The Root Volume UUID Problem
|
|
42
|
+
|
|
43
|
+
The volume mounted at `/` is an **APFS sealed system snapshot** — a
|
|
44
|
+
cryptographically signed, read-only image of the OS. Its UUID changes on
|
|
45
|
+
every macOS update. This makes it unsuitable for persistent identification
|
|
46
|
+
(e.g., licensing fingerprints, asset URIs).
|
|
47
|
+
|
|
48
|
+
The **Data volume** at `/System/Volumes/Data` has a stable UUID and contains
|
|
49
|
+
all user data. Users access it transparently through APFS firmlinks (`/Users`,
|
|
50
|
+
`/Applications`, `/Library`, etc.).
|
|
51
|
+
|
|
52
|
+
### Native detection (Layer 1)
|
|
53
|
+
|
|
54
|
+
Detection uses a combined formula:
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
isSystemVolume = MNT_SNAPSHOT || (MNT_DONTBROWSE && hasApfsRole && role != "Data")
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
This is implemented in `ClassifyMacVolume()` in `src/darwin/system_volume.h`.
|
|
61
|
+
|
|
62
|
+
Each APFS volume has a **role** stored in its superblock (an unsigned 16-bit
|
|
63
|
+
integer). We read this via:
|
|
64
|
+
|
|
65
|
+
1. `DADiskCreateFromBSDName()` to get a DiskArbitration disk ref
|
|
66
|
+
2. `DADiskCopyIOMedia()` to get the IOKit IOMedia service
|
|
67
|
+
3. `IORegistryEntryCreateCFProperty(media, "Role")` to read the role array
|
|
68
|
+
|
|
69
|
+
For APFS snapshots (e.g., `/` is `disk3s7s1`, a snapshot of `disk3s7`), the
|
|
70
|
+
snapshot's IOMedia entry has no `Role` property — we walk one parent up in
|
|
71
|
+
the IOService plane to find the parent volume's role.
|
|
72
|
+
|
|
73
|
+
The role string is exposed as `volumeRole` in both `MountPoint` and
|
|
74
|
+
`VolumeMetadata` (e.g., `"System"`, `"Data"`, `"VM"`).
|
|
75
|
+
|
|
76
|
+
**System volume classification** combines two signals:
|
|
77
|
+
|
|
78
|
+
- **`MNT_SNAPSHOT`** alone marks a volume as system. This catches sealed APFS
|
|
79
|
+
snapshots (`/` and Recovery).
|
|
80
|
+
- **`MNT_DONTBROWSE`** combined with a non-`"Data"` APFS role marks a volume
|
|
81
|
+
as system. This catches all infrastructure volumes (VM, Preboot, Update,
|
|
82
|
+
Hardware, xART, etc.) while correctly excluding the Data volume.
|
|
83
|
+
|
|
84
|
+
**Why this works**: `MNT_DONTBROWSE` means "hidden from Finder" and is set on
|
|
85
|
+
all `/System/Volumes/*` infrastructure mounts. The only false positive would
|
|
86
|
+
be `/System/Volumes/Data`, which has `MNT_DONTBROWSE` but a `"Data"` role —
|
|
87
|
+
so the role check excludes it.
|
|
88
|
+
|
|
89
|
+
**Why not a role whitelist?** The previous approach maintained a whitelist of
|
|
90
|
+
13 system role strings. The flags+exclusion approach is simpler and
|
|
91
|
+
future-proof: if Apple adds new infrastructure roles with `MNT_DONTBROWSE`,
|
|
92
|
+
they're auto-detected without code changes.
|
|
93
|
+
|
|
94
|
+
**Non-APFS `MNT_DONTBROWSE` mounts** (e.g., `devfs` at `/dev`, or a
|
|
95
|
+
hypothetical NFS mount with `nobrowse`) have no APFS role, so the
|
|
96
|
+
`MNT_DONTBROWSE` branch doesn't fire. These fall through to TypeScript
|
|
97
|
+
heuristics (Layer 2).
|
|
98
|
+
|
|
99
|
+
### Native fallback: `MNT_SNAPSHOT` only
|
|
100
|
+
|
|
101
|
+
If a DiskArbitration session can't be created, we fall back to checking only
|
|
102
|
+
`MNT_SNAPSHOT` in the `statfs` `f_flags`. This catches the sealed system
|
|
103
|
+
snapshots (`/` and `/System/Volumes/Recovery`) but misses the other
|
|
104
|
+
infrastructure volumes (VM, Preboot, etc.).
|
|
105
|
+
|
|
106
|
+
### Flag and role summary (observed on macOS 26 Tahoe)
|
|
107
|
+
|
|
108
|
+
| Mount Point | APFS Role | MNT_SNAPSHOT | MNT_DONTBROWSE | isSystemVolume |
|
|
109
|
+
| ---------------------------- | ------------------- | ------------ | -------------- | ---------------- |
|
|
110
|
+
| `/` | System (via parent) | **yes** | no | **yes** |
|
|
111
|
+
| `/System/Volumes/Data` | Data | no | **yes** | no |
|
|
112
|
+
| `/System/Volumes/VM` | VM | no | **yes** | **yes** |
|
|
113
|
+
| `/System/Volumes/Preboot` | Preboot | no | **yes** | **yes** |
|
|
114
|
+
| `/System/Volumes/Recovery` | Recovery | **yes** | no | **yes** |
|
|
115
|
+
| `/System/Volumes/Update` | Update | no | **yes** | **yes** |
|
|
116
|
+
| `/System/Volumes/xarts` | xART | no | **yes** | **yes** |
|
|
117
|
+
| `/System/Volumes/iSCPreboot` | Prelogin | no | **yes** | **yes** |
|
|
118
|
+
| `/System/Volumes/Hardware` | Hardware | no | **yes** | **yes** |
|
|
119
|
+
| `/dev` | _(no IOMedia)_ | no | **yes** | **yes** (fstype) |
|
|
120
|
+
| `/Volumes/sandisk-extreme` | _(no role)_ | no | no | no |
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Linux
|
|
125
|
+
|
|
126
|
+
### Background
|
|
127
|
+
|
|
128
|
+
Linux has no unified "system volume" concept. Instead, the kernel exposes
|
|
129
|
+
dozens of pseudo-filesystems for kernel interfaces, and distributions mount
|
|
130
|
+
various infrastructure paths at boot. Container runtimes add additional
|
|
131
|
+
overlay and bind mounts.
|
|
132
|
+
|
|
133
|
+
There is **no native C++ system volume detection** on Linux. All
|
|
134
|
+
classification happens in TypeScript (Layer 2) using filesystem type matching
|
|
135
|
+
and path pattern globs.
|
|
136
|
+
|
|
137
|
+
### Why no native detection?
|
|
138
|
+
|
|
139
|
+
Unlike macOS (which has APFS roles and `MNT_SNAPSHOT`/`MNT_DONTBROWSE` flags)
|
|
140
|
+
or Windows (which has `CSIDL_WINDOWS` and volume capability flags), Linux has
|
|
141
|
+
no kernel API that directly identifies "this is a system volume." The closest
|
|
142
|
+
signal is the filesystem type from `/proc/self/mounts`, which is already
|
|
143
|
+
available in TypeScript without a native call.
|
|
144
|
+
|
|
145
|
+
### Filesystem type detection
|
|
146
|
+
|
|
147
|
+
The `SystemFsTypesDefault` list in `src/options.ts` identifies pseudo-filesystems
|
|
148
|
+
that don't represent real storage:
|
|
149
|
+
|
|
150
|
+
| Category | Filesystem Types |
|
|
151
|
+
| ------------------------- | ---------------------------------------------------------------------- |
|
|
152
|
+
| Process/kernel interfaces | `proc`, `sysfs`, `debugfs`, `tracefs`, `configfs`, `securityfs`, `bpf` |
|
|
153
|
+
| Device pseudo-filesystems | `devpts`, `devtmpfs` |
|
|
154
|
+
| Memory/temporary | `tmpfs`, `ramfs`, `rootfs`, `hugetlbfs` |
|
|
155
|
+
| Cgroups | `cgroup`, `cgroup2` |
|
|
156
|
+
| Boot/firmware | `efivarfs`, `pstore`, `binfmt_misc` |
|
|
157
|
+
| Automount | `autofs`, `fusectl` |
|
|
158
|
+
| Container/sandbox | `fuse.lxcfs`, `fuse.portal`, `fuse.snapfuse`, `squashfs` |
|
|
159
|
+
| Kernel internal | `nsfs`, `mqueue`, `rpc_pipefs`, `none` |
|
|
160
|
+
|
|
161
|
+
### Path pattern detection
|
|
162
|
+
|
|
163
|
+
The `SystemPathPatternsDefault` list in `src/options.ts` catches system mount
|
|
164
|
+
points by path glob:
|
|
165
|
+
|
|
166
|
+
| Category | Patterns |
|
|
167
|
+
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
168
|
+
| Core system | `/boot`, `/boot/efi`, `/dev`, `/dev/**`, `/proc/**`, `/sys/**` |
|
|
169
|
+
| Runtime | `/run`, `/run/lock`, `/run/credentials/**` |
|
|
170
|
+
| Temporary | `/tmp`, `/var/tmp` |
|
|
171
|
+
| Container runtimes | `/run/docker/**`, `/var/lib/docker/**`, `/run/containerd/**`, `/var/lib/containerd/**`, `/run/containers/**`, `/var/lib/containers/**`, `/var/lib/kubelet/**` |
|
|
172
|
+
| Linux containers | `/var/lib/lxc/**`, `/var/lib/lxd/**` |
|
|
173
|
+
| Snap/Flatpak | `/snap/**`, `/run/snapd/**`, `/run/flatpak/**`, `/run/user/*/doc`, `/run/user/*/gvfs` |
|
|
174
|
+
| WSL infrastructure | `/mnt/wslg/distro`, `/mnt/wslg/doc`, `/usr/lib/wsl/drivers` |
|
|
175
|
+
| Snapshot dirs | `**/#snapshot` |
|
|
176
|
+
|
|
177
|
+
### Typical detection results
|
|
178
|
+
|
|
179
|
+
| Mount Point | fstype | Detected By | isSystemVolume |
|
|
180
|
+
| ------------------------------ | ---------- | ------------- | -------------- |
|
|
181
|
+
| `/` | `ext4` | _(none)_ | no |
|
|
182
|
+
| `/boot` | `ext4` | path | **yes** |
|
|
183
|
+
| `/boot/efi` | `vfat` | path | **yes** |
|
|
184
|
+
| `/dev` | `devtmpfs` | fstype + path | **yes** |
|
|
185
|
+
| `/proc` | `proc` | fstype + path | **yes** |
|
|
186
|
+
| `/sys` | `sysfs` | fstype + path | **yes** |
|
|
187
|
+
| `/run` | `tmpfs` | fstype + path | **yes** |
|
|
188
|
+
| `/tmp` | `tmpfs` | fstype + path | **yes** |
|
|
189
|
+
| `/home` | `ext4` | _(none)_ | no |
|
|
190
|
+
| `/var/lib/docker/overlay2/...` | `overlay` | path | **yes** |
|
|
191
|
+
| `/mnt/data` | `ext4` | _(none)_ | no |
|
|
192
|
+
|
|
193
|
+
### Customization
|
|
194
|
+
|
|
195
|
+
Both lists are configurable via `Options.systemFsTypes` and
|
|
196
|
+
`Options.systemPathPatterns`, allowing callers to add or replace detection
|
|
197
|
+
rules for specialized environments.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Windows
|
|
202
|
+
|
|
203
|
+
### Background
|
|
204
|
+
|
|
205
|
+
Windows has a simpler model: drives are identified by letter (`C:\`, `D:\`,
|
|
206
|
+
etc.), and one drive is the "system drive" containing the Windows
|
|
207
|
+
installation. Unlike macOS's split-volume architecture, the system drive also
|
|
208
|
+
holds user data (`C:\Users`).
|
|
209
|
+
|
|
210
|
+
### Native detection (Layer 1)
|
|
211
|
+
|
|
212
|
+
`IsSystemVolume()` in `src/windows/system_volume.h` uses two checks:
|
|
213
|
+
|
|
214
|
+
1. **`SHGetFolderPathW(CSIDL_WINDOWS)`** — retrieves the Windows system
|
|
215
|
+
folder path (e.g., `C:\Windows`), extracts the drive letter, and compares
|
|
216
|
+
it against the volume being tested. This is the primary detection method.
|
|
217
|
+
|
|
218
|
+
2. **Volume capability flags** — calls `GetVolumeInformationW()` and checks
|
|
219
|
+
for `FILE_SUPPORTS_SYSTEM_PATHS` (0x00100000) and
|
|
220
|
+
`FILE_SUPPORTS_SYSTEM_FILES` (0x00200000). These are modern (Windows 10+)
|
|
221
|
+
volume flags that indicate system volume capabilities.
|
|
222
|
+
|
|
223
|
+
### TypeScript detection (Layer 2)
|
|
224
|
+
|
|
225
|
+
The TypeScript layer (`src/system_volume.ts`) provides a redundant check using
|
|
226
|
+
`process.env.SystemDrive` (typically `"C:"`), normalized to `"C:\"` for
|
|
227
|
+
comparison.
|
|
228
|
+
|
|
229
|
+
### Why `includeSystemVolumes` defaults to `true` on Windows
|
|
230
|
+
|
|
231
|
+
On macOS and Linux, system volumes (pseudo-filesystems, sealed snapshots) are
|
|
232
|
+
genuinely uninteresting to most callers. On Windows, `C:\` is both the system
|
|
233
|
+
drive _and_ the primary user storage location — excluding it would hide the
|
|
234
|
+
most important volume. So `IncludeSystemVolumesDefault = true` on Windows
|
|
235
|
+
(see `src/options.ts`).
|
|
236
|
+
|
|
237
|
+
### Typical detection results
|
|
238
|
+
|
|
239
|
+
| Mount Point | Detection Method | isSystemVolume |
|
|
240
|
+
| ---------------- | --------------------------- | -------------- |
|
|
241
|
+
| `C:\` | CSIDL_WINDOWS + SystemDrive | **yes** |
|
|
242
|
+
| `D:\` | _(none)_ | no |
|
|
243
|
+
| `E:\` | _(none)_ | no |
|
|
244
|
+
| `\\server\share` | _(none)_ | no |
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Layer 2: TypeScript heuristics (all platforms)
|
|
249
|
+
|
|
250
|
+
The `assignSystemVolume()` function in `src/system_volume.ts` runs after native
|
|
251
|
+
enumeration and applies the shared heuristic layer:
|
|
252
|
+
|
|
253
|
+
1. On Windows, checks `process.env.SystemDrive`
|
|
254
|
+
2. Checks `fstype` against `SystemFsTypesDefault` (configurable)
|
|
255
|
+
3. Checks `mountPoint` against `SystemPathPatternsDefault` glob patterns
|
|
256
|
+
(configurable)
|
|
257
|
+
|
|
258
|
+
Native `isSystemVolume: true` is **never downgraded** — only upgraded from
|
|
259
|
+
`false` to `true`.
|
|
260
|
+
|
|
261
|
+
## Related Files
|
|
262
|
+
|
|
263
|
+
- `src/darwin/system_volume.h` — `ClassifyMacVolume()`: macOS flags + APFS role detection
|
|
264
|
+
- `src/darwin/raii_utils.h` — RAII wrappers for DA/CF/IOKit resources
|
|
265
|
+
- `src/windows/system_volume.h` — `IsSystemVolume()`: Windows CSIDL + volume flags
|
|
266
|
+
- `src/options.ts` — `SystemFsTypesDefault`, `SystemPathPatternsDefault`, `IncludeSystemVolumesDefault`
|
|
267
|
+
- `src/system_volume.ts` — `isSystemVolume()`, `assignSystemVolume()`: TypeScript heuristic layer
|
|
268
|
+
- `src/types/mount_point.ts` — `MountPoint.volumeRole` / `isSystemVolume` / `isReadOnly` fields
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@photostructure/fs-metadata",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Cross-platform native filesystem metadata retrieval for Node.js",
|
|
5
5
|
"homepage": "https://photostructure.github.io/fs-metadata/",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"fmt:pkg": "npm pkg fix",
|
|
59
59
|
"update": "run-p update:*",
|
|
60
60
|
"update:deps": "ncu -u",
|
|
61
|
-
"install:pinact": "go install github.com/suzuki-shunsuke/pinact/cmd/pinact@latest",
|
|
61
|
+
"install:pinact": "go install github.com/suzuki-shunsuke/pinact/v3/cmd/pinact@latest",
|
|
62
62
|
"update:actions": "pinact run -u || echo \"problem with pinact\"",
|
|
63
63
|
"// precommit": "should be manually run by developers before they run `git commit`",
|
|
64
64
|
"precommit": "npx --yes tsx scripts/precommit.ts",
|
|
@@ -91,34 +91,34 @@
|
|
|
91
91
|
"cross-platform"
|
|
92
92
|
],
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"node-addon-api": "^8.
|
|
94
|
+
"node-addon-api": "^8.7.0",
|
|
95
95
|
"node-gyp-build": "^4.8.4"
|
|
96
96
|
},
|
|
97
97
|
"devDependencies": {
|
|
98
98
|
"@types/jest": "^30.0.0",
|
|
99
|
-
"@types/node": "^25.
|
|
99
|
+
"@types/node": "^25.5.0",
|
|
100
100
|
"@types/semver": "^7.7.1",
|
|
101
101
|
"cross-env": "^10.1.0",
|
|
102
102
|
"del-cli": "^7.0.0",
|
|
103
103
|
"eslint": "9.39.1",
|
|
104
|
-
"eslint-plugin-regexp": "^3.
|
|
104
|
+
"eslint-plugin-regexp": "^3.1.0",
|
|
105
105
|
"eslint-plugin-security": "^4.0.0",
|
|
106
|
-
"globals": "^17.
|
|
107
|
-
"jest": "^30.
|
|
108
|
-
"jest-environment-node": "^30.
|
|
106
|
+
"globals": "^17.4.0",
|
|
107
|
+
"jest": "^30.3.0",
|
|
108
|
+
"jest-environment-node": "^30.3.0",
|
|
109
109
|
"jest-extended": "^7.0.0",
|
|
110
110
|
"node-gyp": "^12.2.0",
|
|
111
|
-
"npm-check-updates": "^19.
|
|
111
|
+
"npm-check-updates": "^19.6.6",
|
|
112
112
|
"npm-run-all2": "8.0.4",
|
|
113
113
|
"prebuildify": "^6.0.1",
|
|
114
114
|
"prettier": "^3.8.1",
|
|
115
115
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
116
|
-
"terser": "^5.46.
|
|
116
|
+
"terser": "^5.46.1",
|
|
117
117
|
"ts-jest": "^29.4.6",
|
|
118
118
|
"tsup": "^8.5.1",
|
|
119
119
|
"tsx": "^4.21.0",
|
|
120
|
-
"typedoc": "^0.28.
|
|
120
|
+
"typedoc": "^0.28.18",
|
|
121
121
|
"typescript": "^5.9.3",
|
|
122
|
-
"typescript-eslint": "^8.
|
|
122
|
+
"typescript-eslint": "^8.57.2"
|
|
123
123
|
}
|
|
124
124
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/binding.cpp
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
#include "windows/hidden.h"
|
|
9
9
|
#elif defined(__APPLE__)
|
|
10
10
|
#include "darwin/fs_meta.h"
|
|
11
|
+
#include "darwin/get_mount_point.h"
|
|
11
12
|
#include "darwin/hidden.h"
|
|
12
13
|
#elif defined(__linux__)
|
|
13
14
|
#include "common/volume_metadata.h"
|
|
@@ -60,6 +61,12 @@ Napi::Value GetVolumeMetadata(const Napi::CallbackInfo &info) {
|
|
|
60
61
|
return FSMeta::GetVolumeMetadata(info);
|
|
61
62
|
}
|
|
62
63
|
|
|
64
|
+
#if defined(__APPLE__)
|
|
65
|
+
Napi::Value GetMountPointForPath(const Napi::CallbackInfo &info) {
|
|
66
|
+
return FSMeta::GetMountPoint(info);
|
|
67
|
+
}
|
|
68
|
+
#endif
|
|
69
|
+
|
|
63
70
|
#if defined(_WIN32) || defined(__APPLE__)
|
|
64
71
|
Napi::Value GetHiddenAttribute(const Napi::CallbackInfo &info) {
|
|
65
72
|
return FSMeta::GetHiddenAttribute(info);
|
|
@@ -81,6 +88,10 @@ Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
|
81
88
|
|
|
82
89
|
exports.Set("getVolumeMetadata", Napi::Function::New(env, GetVolumeMetadata));
|
|
83
90
|
|
|
91
|
+
#if defined(__APPLE__)
|
|
92
|
+
exports.Set("getMountPoint", Napi::Function::New(env, GetMountPointForPath));
|
|
93
|
+
#endif
|
|
94
|
+
|
|
84
95
|
#ifdef ENABLE_GIO
|
|
85
96
|
exports.Set("getGioMountPoints", Napi::Function::New(env, GetGioMountPoints));
|
|
86
97
|
#endif
|
|
@@ -40,9 +40,9 @@ struct VolumeMetadataOptions {
|
|
|
40
40
|
struct VolumeMetadata {
|
|
41
41
|
std::string label;
|
|
42
42
|
std::string fstype;
|
|
43
|
-
double size;
|
|
44
|
-
double used;
|
|
45
|
-
double available;
|
|
43
|
+
double size = 0.0;
|
|
44
|
+
double used = 0.0;
|
|
45
|
+
double available = 0.0;
|
|
46
46
|
std::string uuid;
|
|
47
47
|
std::string mountFrom;
|
|
48
48
|
std::string mountName;
|
|
@@ -52,6 +52,8 @@ struct VolumeMetadata {
|
|
|
52
52
|
std::string remoteHost;
|
|
53
53
|
std::string remoteShare;
|
|
54
54
|
bool isSystemVolume = false;
|
|
55
|
+
bool isReadOnly = false;
|
|
56
|
+
std::string volumeRole;
|
|
55
57
|
std::string error;
|
|
56
58
|
|
|
57
59
|
Napi::Object ToObject(Napi::Env env) const {
|
|
@@ -120,6 +122,11 @@ struct VolumeMetadata {
|
|
|
120
122
|
}
|
|
121
123
|
|
|
122
124
|
result.Set("isSystemVolume", Napi::Boolean::New(env, isSystemVolume));
|
|
125
|
+
result.Set("isReadOnly", Napi::Boolean::New(env, isReadOnly));
|
|
126
|
+
|
|
127
|
+
if (!volumeRole.empty()) {
|
|
128
|
+
result.Set("volumeRole", Napi::String::New(env, volumeRole));
|
|
129
|
+
}
|
|
123
130
|
|
|
124
131
|
return result;
|
|
125
132
|
}
|
|
@@ -22,7 +22,9 @@ struct MountPoint {
|
|
|
22
22
|
std::string mountPoint;
|
|
23
23
|
std::string fstype;
|
|
24
24
|
std::string status;
|
|
25
|
-
bool isSystemVolume = false;
|
|
25
|
+
bool isSystemVolume = false;
|
|
26
|
+
bool isReadOnly = false;
|
|
27
|
+
std::string volumeRole;
|
|
26
28
|
std::string error;
|
|
27
29
|
|
|
28
30
|
Napi::Object ToObject(Napi::Env env) const {
|
|
@@ -38,6 +40,10 @@ struct MountPoint {
|
|
|
38
40
|
obj.Set("status", status);
|
|
39
41
|
}
|
|
40
42
|
obj.Set("isSystemVolume", isSystemVolume);
|
|
43
|
+
obj.Set("isReadOnly", isReadOnly);
|
|
44
|
+
if (!volumeRole.empty()) {
|
|
45
|
+
obj.Set("volumeRole", volumeRole);
|
|
46
|
+
}
|
|
41
47
|
obj.Set("error", error);
|
|
42
48
|
return obj;
|
|
43
49
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// src/darwin/da_mutex.h
|
|
2
|
+
//
|
|
3
|
+
// Shared mutex for DiskArbitration operations.
|
|
4
|
+
//
|
|
5
|
+
// Apple's DiskArbitration framework does not document thread safety for
|
|
6
|
+
// concurrent DASession usage across threads. To prevent data races, all DA
|
|
7
|
+
// operations (session creation, disk description, IOKit queries via
|
|
8
|
+
// ClassifyMacVolume) must be serialized through this mutex.
|
|
9
|
+
//
|
|
10
|
+
// See: Finding #5 in SECURITY_AUDIT_2025.md (original)
|
|
11
|
+
// Finding #2 in SECURITY_AUDIT_2026.md (mount points regression)
|
|
12
|
+
|
|
13
|
+
#pragma once
|
|
14
|
+
|
|
15
|
+
#include <mutex>
|
|
16
|
+
|
|
17
|
+
namespace FSMeta {
|
|
18
|
+
|
|
19
|
+
// Defined in volume_metadata.cpp. Serializes all DiskArbitration + IOKit
|
|
20
|
+
// operations across both getVolumeMetadata and getVolumeMountPoints workers.
|
|
21
|
+
extern std::mutex g_diskArbitrationMutex;
|
|
22
|
+
|
|
23
|
+
} // namespace FSMeta
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// src/darwin/get_mount_point.cpp
|
|
2
|
+
// Lightweight mount point lookup using fstatfs() only.
|
|
3
|
+
// Returns f_mntonname without DiskArbitration, IOKit, or space calculations.
|
|
4
|
+
|
|
5
|
+
#include "./get_mount_point.h"
|
|
6
|
+
#include "../common/debug_log.h"
|
|
7
|
+
#include "../common/error_utils.h"
|
|
8
|
+
#include "../common/fd_guard.h"
|
|
9
|
+
#include "../common/path_security.h"
|
|
10
|
+
|
|
11
|
+
#include <fcntl.h>
|
|
12
|
+
#include <string>
|
|
13
|
+
#include <sys/mount.h>
|
|
14
|
+
#include <sys/param.h>
|
|
15
|
+
#include <unistd.h>
|
|
16
|
+
|
|
17
|
+
namespace FSMeta {
|
|
18
|
+
|
|
19
|
+
class GetMountPointWorker : public Napi::AsyncWorker {
|
|
20
|
+
public:
|
|
21
|
+
GetMountPointWorker(const std::string &path,
|
|
22
|
+
const Napi::Promise::Deferred &deferred)
|
|
23
|
+
: Napi::AsyncWorker(deferred.Env()), path_(path), deferred_(deferred) {}
|
|
24
|
+
|
|
25
|
+
void Execute() override {
|
|
26
|
+
DEBUG_LOG("[GetMountPointWorker] Executing for path: %s", path_.c_str());
|
|
27
|
+
try {
|
|
28
|
+
std::string error;
|
|
29
|
+
std::string validated = ValidatePathForRead(path_, error);
|
|
30
|
+
if (validated.empty()) {
|
|
31
|
+
SetError(error);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
DEBUG_LOG("[GetMountPointWorker] Using validated path: %s",
|
|
36
|
+
validated.c_str());
|
|
37
|
+
|
|
38
|
+
int fd = open(validated.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
39
|
+
if (fd < 0) {
|
|
40
|
+
int err = errno;
|
|
41
|
+
DEBUG_LOG("[GetMountPointWorker] open failed: %s (%d)", strerror(err),
|
|
42
|
+
err);
|
|
43
|
+
SetError(CreatePathErrorMessage("open", path_, err));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
FdGuard guard(fd);
|
|
48
|
+
|
|
49
|
+
struct statfs fs;
|
|
50
|
+
if (fstatfs(fd, &fs) != 0) {
|
|
51
|
+
int err = errno;
|
|
52
|
+
DEBUG_LOG("[GetMountPointWorker] fstatfs failed: %s (%d)",
|
|
53
|
+
strerror(err), err);
|
|
54
|
+
SetError(CreatePathErrorMessage("fstatfs", path_, err));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
result_ = fs.f_mntonname;
|
|
59
|
+
DEBUG_LOG("[GetMountPointWorker] mount point: %s", result_.c_str());
|
|
60
|
+
} catch (const std::exception &e) {
|
|
61
|
+
DEBUG_LOG("[GetMountPointWorker] Exception: %s", e.what());
|
|
62
|
+
SetError(e.what());
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
void OnOK() override {
|
|
67
|
+
Napi::HandleScope scope(Env());
|
|
68
|
+
deferred_.Resolve(Napi::String::New(Env(), result_));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
void OnError(const Napi::Error &error) override {
|
|
72
|
+
deferred_.Reject(error.Value());
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private:
|
|
76
|
+
std::string path_;
|
|
77
|
+
std::string result_;
|
|
78
|
+
Napi::Promise::Deferred deferred_;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
Napi::Value GetMountPoint(const Napi::CallbackInfo &info) {
|
|
82
|
+
auto env = info.Env();
|
|
83
|
+
DEBUG_LOG("[GetMountPoint] called");
|
|
84
|
+
|
|
85
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
86
|
+
throw Napi::TypeError::New(env, "String argument expected");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
std::string path = info[0].As<Napi::String>().Utf8Value();
|
|
90
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
91
|
+
auto *worker = new GetMountPointWorker(path, deferred);
|
|
92
|
+
worker->Queue();
|
|
93
|
+
return deferred.Promise();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
} // namespace FSMeta
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// src/darwin/get_mount_point.h
|
|
2
|
+
// Lightweight mount point lookup using fstatfs() only.
|
|
3
|
+
// Returns f_mntonname without DiskArbitration, IOKit, or space calculations.
|
|
4
|
+
|
|
5
|
+
#pragma once
|
|
6
|
+
|
|
7
|
+
#include <napi.h>
|
|
8
|
+
|
|
9
|
+
namespace FSMeta {
|
|
10
|
+
|
|
11
|
+
Napi::Value GetMountPoint(const Napi::CallbackInfo &info);
|
|
12
|
+
|
|
13
|
+
} // namespace FSMeta
|
package/src/darwin/raii_utils.h
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
#include <CoreFoundation/CoreFoundation.h>
|
|
4
4
|
#include <DiskArbitration/DiskArbitration.h>
|
|
5
|
+
#include <IOKit/IOKitLib.h>
|
|
5
6
|
#include <sys/mount.h>
|
|
6
7
|
|
|
7
8
|
// RAII (Resource Acquisition Is Initialization) utilities for macOS APIs.
|
|
@@ -127,6 +128,44 @@ public:
|
|
|
127
128
|
}
|
|
128
129
|
};
|
|
129
130
|
|
|
131
|
+
// RAII wrapper for IOKit io_object_t handles (io_service_t,
|
|
132
|
+
// io_registry_entry_t). IOKit objects must be released with IOObjectRelease(),
|
|
133
|
+
// not CFRelease().
|
|
134
|
+
class IOObjectGuard {
|
|
135
|
+
private:
|
|
136
|
+
io_object_t obj_;
|
|
137
|
+
|
|
138
|
+
public:
|
|
139
|
+
explicit IOObjectGuard(io_object_t obj = 0) noexcept : obj_(obj) {}
|
|
140
|
+
~IOObjectGuard() noexcept {
|
|
141
|
+
if (obj_) {
|
|
142
|
+
IOObjectRelease(obj_);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
io_object_t get() const noexcept { return obj_; }
|
|
147
|
+
bool isValid() const noexcept { return obj_ != 0; }
|
|
148
|
+
|
|
149
|
+
// Prevent copying
|
|
150
|
+
IOObjectGuard(const IOObjectGuard &) = delete;
|
|
151
|
+
IOObjectGuard &operator=(const IOObjectGuard &) = delete;
|
|
152
|
+
|
|
153
|
+
// Allow moving
|
|
154
|
+
IOObjectGuard(IOObjectGuard &&other) noexcept : obj_(other.obj_) {
|
|
155
|
+
other.obj_ = 0;
|
|
156
|
+
}
|
|
157
|
+
IOObjectGuard &operator=(IOObjectGuard &&other) noexcept {
|
|
158
|
+
if (this != &other) {
|
|
159
|
+
if (obj_) {
|
|
160
|
+
IOObjectRelease(obj_);
|
|
161
|
+
}
|
|
162
|
+
obj_ = other.obj_;
|
|
163
|
+
other.obj_ = 0;
|
|
164
|
+
}
|
|
165
|
+
return *this;
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
|
|
130
169
|
// Specialized RAII wrapper for DASession that handles dispatch queue lifecycle.
|
|
131
170
|
// DASessionSetDispatchQueue must be called with NULL before the session is
|
|
132
171
|
// released. This wrapper ensures proper cleanup order: unschedule then release.
|