@photostructure/fs-metadata 0.7.0 → 0.8.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 +34 -1
- package/CLAUDE.md +1 -1
- package/CONTRIBUTING.md +15 -0
- package/README.md +2 -1
- package/dist/index.cjs +11 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +10 -5
- package/dist/index.d.mts +10 -5
- package/dist/index.d.ts +10 -5
- package/dist/index.mjs +10 -3
- package/dist/index.mjs.map +1 -1
- package/doc/LINUX_API_REFERENCE.md +310 -0
- package/doc/MACOS_API_REFERENCE.md +367 -31
- package/doc/WINDOWS_API_REFERENCE.md +35 -2
- package/doc/gotchas.md +28 -0
- package/package.json +17 -20
- 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/precommit.ts +4 -1
- package/src/common/fd_guard.h +71 -0
- package/src/{darwin → common}/path_security.h +8 -5
- package/src/common/volume_utils.h +51 -0
- package/src/darwin/hidden.cpp +47 -14
- package/src/darwin/raii_utils.h +8 -8
- package/src/darwin/volume_metadata.cpp +33 -39
- package/src/index.ts +3 -3
- package/src/linux/blkid_cache.cpp +5 -11
- package/src/linux/blkid_cache.h +21 -0
- package/src/linux/gio_utils.cpp +7 -23
- package/src/linux/gio_utils.h +16 -40
- package/src/linux/gio_volume_metadata.cpp +16 -88
- package/src/linux/volume_metadata.cpp +35 -27
- package/src/options.ts +16 -3
- package/src/types/options.ts +1 -1
- package/src/windows/drive_status.h +74 -49
- package/src/windows/error_utils.h +2 -2
- package/src/windows/security_utils.h +47 -2
- package/src/windows/thread_pool.h +29 -4
- package/src/windows/volume_metadata.cpp +17 -12
|
@@ -5,12 +5,28 @@ This document provides comprehensive documentation for all macOS APIs used in th
|
|
|
5
5
|
## Table of Contents
|
|
6
6
|
|
|
7
7
|
1. [Core Foundation APIs](#core-foundation-apis)
|
|
8
|
+
- [CFString](#cfstring)
|
|
9
|
+
- [CFDictionary](#cfdictionary)
|
|
10
|
+
- [CFReleaser RAII Wrapper](#cfreleaser-raii-wrapper)
|
|
8
11
|
2. [DiskArbitration Framework](#diskarbitration-framework)
|
|
12
|
+
- [DASession](#dasession)
|
|
13
|
+
- [DASessionSetDispatchQueue](#dasessionsetdispatchqueue)
|
|
14
|
+
- [DADisk](#dadisk)
|
|
9
15
|
3. [File System APIs](#file-system-apis)
|
|
16
|
+
- [getmntinfo_r_np](#getmntinfo_r_np)
|
|
17
|
+
- [statfs64](#statfs64)
|
|
18
|
+
- [chflags and fchflags](#chflags-and-fchflags)
|
|
19
|
+
- [open() Flags (Darwin-Specific)](#open-flags-darwin-specific)
|
|
20
|
+
- [fstat, fstatfs, fstatvfs](#fstat-fstatfs-fstatvfs-toctou-safe-variants)
|
|
10
21
|
4. [Security APIs](#security-apis)
|
|
22
|
+
- [faccessat](#faccessat)
|
|
23
|
+
- [realpath()](#realpath---path-canonicalization)
|
|
11
24
|
5. [RAII Patterns and Memory Management](#raii-patterns-and-memory-management)
|
|
25
|
+
- [Memory Management Rules](#memory-management-rules)
|
|
12
26
|
6. [Thread Safety Considerations](#thread-safety-considerations)
|
|
13
27
|
7. [Error Handling Patterns](#error-handling-patterns)
|
|
28
|
+
8. [Platform-Specific Considerations](#platform-specific-considerations)
|
|
29
|
+
9. [References](#references)
|
|
14
30
|
|
|
15
31
|
## Core Foundation APIs
|
|
16
32
|
|
|
@@ -129,11 +145,84 @@ if (!session.get()) {
|
|
|
129
145
|
|
|
130
146
|
**Apple Documentation**: [DiskArbitration Framework](https://developer.apple.com/documentation/diskarbitration)
|
|
131
147
|
|
|
148
|
+
**DiskArbitration Programming Guide**: [Apple Archive](https://developer.apple.com/library/archive/documentation/DriversKernelHardware/Conceptual/DiskArbitrationProgGuide/Introduction/Introduction.html)
|
|
149
|
+
|
|
132
150
|
**Important Notes**:
|
|
133
151
|
|
|
134
152
|
- Must be protected by mutex for thread safety
|
|
135
153
|
- Session should be short-lived
|
|
136
|
-
-
|
|
154
|
+
- Requires scheduling on a dispatch queue or run loop before use
|
|
155
|
+
|
|
156
|
+
### DASessionSetDispatchQueue
|
|
157
|
+
|
|
158
|
+
**Purpose**: Schedule a DiskArbitration session on a dispatch queue for callbacks.
|
|
159
|
+
|
|
160
|
+
**Apple's Documented Pattern**:
|
|
161
|
+
|
|
162
|
+
1. Create session with `DASessionCreate()`
|
|
163
|
+
2. Schedule on dispatch queue with `DASessionSetDispatchQueue()`
|
|
164
|
+
3. Use the session for disk operations
|
|
165
|
+
4. Unschedule by calling `DASessionSetDispatchQueue(session, NULL)`
|
|
166
|
+
5. Release the session
|
|
167
|
+
|
|
168
|
+
**Usage in Project**:
|
|
169
|
+
|
|
170
|
+
```cpp
|
|
171
|
+
// Create session
|
|
172
|
+
DASessionRef session = DASessionCreate(kCFAllocatorDefault);
|
|
173
|
+
if (!session) {
|
|
174
|
+
throw std::runtime_error("Failed to create DA session");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Schedule on a serial queue (NOT main queue in Node.js context)
|
|
178
|
+
// Static queue is intentional - singleton for process lifetime
|
|
179
|
+
static dispatch_queue_t da_queue = dispatch_queue_create(
|
|
180
|
+
"com.example.diskarbitration", DISPATCH_QUEUE_SERIAL);
|
|
181
|
+
DASessionSetDispatchQueue(session, da_queue);
|
|
182
|
+
|
|
183
|
+
// Use session...
|
|
184
|
+
DADiskRef disk = DADiskCreateFromBSDName(kCFAllocatorDefault, session, "disk1s1");
|
|
185
|
+
CFDictionaryRef desc = DADiskCopyDescription(disk);
|
|
186
|
+
|
|
187
|
+
// CRITICAL: Unschedule before release
|
|
188
|
+
DASessionSetDispatchQueue(session, NULL);
|
|
189
|
+
CFRelease(session);
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Why We Use a Static Dispatch Queue**:
|
|
193
|
+
|
|
194
|
+
The dispatch queue is intentionally static (process lifetime) because:
|
|
195
|
+
|
|
196
|
+
1. Creating/destroying queues is expensive
|
|
197
|
+
2. Releasing while operations in-flight could race
|
|
198
|
+
3. GCD queues are lightweight references to internal structures
|
|
199
|
+
4. This pattern is standard for long-lived resources in macOS apps
|
|
200
|
+
|
|
201
|
+
**RAII Wrapper for Safe Session Management**:
|
|
202
|
+
|
|
203
|
+
```cpp
|
|
204
|
+
class DASessionRAII {
|
|
205
|
+
CFReleaser<DASessionRef> session_;
|
|
206
|
+
bool is_scheduled_ = false;
|
|
207
|
+
public:
|
|
208
|
+
explicit DASessionRAII(DASessionRef s) : session_(s) {}
|
|
209
|
+
|
|
210
|
+
void scheduleOnQueue(dispatch_queue_t queue) {
|
|
211
|
+
if (session_.isValid() && queue) {
|
|
212
|
+
DASessionSetDispatchQueue(session_.get(), queue);
|
|
213
|
+
is_scheduled_ = true;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
~DASessionRAII() noexcept {
|
|
218
|
+
// CRITICAL: Unschedule before CFRelease
|
|
219
|
+
if (is_scheduled_ && session_.isValid()) {
|
|
220
|
+
DASessionSetDispatchQueue(session_.get(), nullptr);
|
|
221
|
+
}
|
|
222
|
+
// CFReleaser handles the CFRelease
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
```
|
|
137
226
|
|
|
138
227
|
### DADisk
|
|
139
228
|
|
|
@@ -227,17 +316,27 @@ if (statfs64(path.c_str(), &buf) == 0) {
|
|
|
227
316
|
- `f_bsize` - Block size
|
|
228
317
|
- `f_fstypename` - File system type name
|
|
229
318
|
|
|
230
|
-
### chflags
|
|
319
|
+
### chflags and fchflags
|
|
231
320
|
|
|
232
321
|
**Purpose**: Set file flags (including hidden attribute).
|
|
233
322
|
|
|
234
|
-
**
|
|
323
|
+
**IMPORTANT**: Prefer `fchflags()` over `chflags()` to prevent TOCTOU race conditions.
|
|
324
|
+
|
|
325
|
+
**Secure Usage (TOCTOU-safe)**:
|
|
235
326
|
|
|
236
327
|
```cpp
|
|
237
|
-
//
|
|
328
|
+
// Open file first to get a stable reference
|
|
329
|
+
int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC);
|
|
330
|
+
if (fd < 0) {
|
|
331
|
+
SetError(CreateDetailedErrorMessage("open", errno));
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
FdGuard fd_guard(fd); // RAII for automatic close
|
|
335
|
+
|
|
336
|
+
// Get current flags via file descriptor
|
|
238
337
|
struct stat st;
|
|
239
|
-
if (
|
|
240
|
-
SetError(CreateDetailedErrorMessage("
|
|
338
|
+
if (fstat(fd, &st) != 0) {
|
|
339
|
+
SetError(CreateDetailedErrorMessage("fstat", errno));
|
|
241
340
|
return;
|
|
242
341
|
}
|
|
243
342
|
|
|
@@ -249,19 +348,135 @@ if (hidden_) {
|
|
|
249
348
|
flags &= ~UF_HIDDEN; // Clear hidden
|
|
250
349
|
}
|
|
251
350
|
|
|
252
|
-
// Apply new flags
|
|
253
|
-
if (
|
|
254
|
-
SetError(CreateDetailedErrorMessage("
|
|
351
|
+
// Apply new flags via file descriptor (TOCTOU-safe)
|
|
352
|
+
if (fchflags(fd, flags) != 0) {
|
|
353
|
+
SetError(CreateDetailedErrorMessage("fchflags", errno));
|
|
255
354
|
}
|
|
256
355
|
```
|
|
257
356
|
|
|
357
|
+
**Legacy Usage (NOT recommended - TOCTOU vulnerable)**:
|
|
358
|
+
|
|
359
|
+
```cpp
|
|
360
|
+
// DON'T DO THIS - vulnerable to race conditions
|
|
361
|
+
struct stat st;
|
|
362
|
+
if (stat(path_.c_str(), &st) != 0) { /* ... */ }
|
|
363
|
+
// WINDOW: File could be replaced here!
|
|
364
|
+
if (chflags(path_.c_str(), flags) != 0) { /* ... */ }
|
|
365
|
+
```
|
|
366
|
+
|
|
258
367
|
**Apple Documentation**: [chflags(2)](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man2/chflags.2.html)
|
|
259
368
|
|
|
369
|
+
**FreeBSD Documentation** (macOS derives from BSD): [fchflags(2)](https://man.freebsd.org/cgi/man.cgi?query=fchflags&sektion=2)
|
|
370
|
+
|
|
371
|
+
**Function Signatures**:
|
|
372
|
+
|
|
373
|
+
```c
|
|
374
|
+
int chflags(const char *path, u_int flags); // Path-based (TOCTOU risk)
|
|
375
|
+
int fchflags(int fd, u_int flags); // FD-based (TOCTOU-safe)
|
|
376
|
+
int lchflags(const char *path, u_int flags); // Operates on symlink itself
|
|
377
|
+
```
|
|
378
|
+
|
|
260
379
|
**Common Flags**:
|
|
261
380
|
|
|
262
|
-
- `UF_HIDDEN` - Hidden file flag
|
|
263
|
-
- `UF_IMMUTABLE` - File cannot be changed
|
|
264
|
-
- `
|
|
381
|
+
- `UF_HIDDEN` - Hidden file flag (user-settable)
|
|
382
|
+
- `UF_IMMUTABLE` - File cannot be changed (user-settable)
|
|
383
|
+
- `UF_APPEND` - File may only be appended to
|
|
384
|
+
- `SF_ARCHIVED` - File has been archived (super-user only)
|
|
385
|
+
- `SF_IMMUTABLE` - File cannot be changed (super-user only)
|
|
386
|
+
|
|
387
|
+
**Error Codes for fchflags()**:
|
|
388
|
+
|
|
389
|
+
- `EBADF` - Invalid file descriptor
|
|
390
|
+
- `EINVAL` - fd refers to a socket, not a file
|
|
391
|
+
- `EPERM` - Insufficient permissions to change flags
|
|
392
|
+
- `EROFS` - File resides on read-only filesystem
|
|
393
|
+
- `ENOTSUP` - Filesystem doesn't support file flags
|
|
394
|
+
|
|
395
|
+
### open() Flags (Darwin-Specific)
|
|
396
|
+
|
|
397
|
+
**Purpose**: Control file opening behavior for security and resource management.
|
|
398
|
+
|
|
399
|
+
**Darwin XNU fcntl.h Source**: [apple/darwin-xnu fcntl.h](https://github.com/apple/darwin-xnu/blob/main/bsd/sys/fcntl.h)
|
|
400
|
+
|
|
401
|
+
**Critical Flags**:
|
|
402
|
+
|
|
403
|
+
| Flag | Value | Purpose |
|
|
404
|
+
| ------------- | ------------ | ----------------------------------------------------- |
|
|
405
|
+
| `O_CLOEXEC` | `0x01000000` | Close fd on exec(), prevents leaks to child processes |
|
|
406
|
+
| `O_NOFOLLOW` | `0x00000100` | Fail with `ELOOP` if path is a symlink |
|
|
407
|
+
| `O_SYMLINK` | `0x00200000` | Open the symlink itself, not its target |
|
|
408
|
+
| `O_DIRECTORY` | `0x00100000` | Fail if not a directory |
|
|
409
|
+
|
|
410
|
+
**O_CLOEXEC - Preventing File Descriptor Leaks**:
|
|
411
|
+
|
|
412
|
+
```cpp
|
|
413
|
+
// ALWAYS use O_CLOEXEC when opening files in a Node.js native module
|
|
414
|
+
// This prevents fd leaks when Node.js forks child processes
|
|
415
|
+
int fd = open(path.c_str(), O_RDONLY | O_CLOEXEC);
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
Without `O_CLOEXEC`, file descriptors leak to child processes created via `fork()`/`exec()`, potentially causing resource exhaustion or security issues.
|
|
419
|
+
|
|
420
|
+
**O_NOFOLLOW vs O_SYMLINK**:
|
|
421
|
+
|
|
422
|
+
```cpp
|
|
423
|
+
// O_NOFOLLOW: Fail if target is a symlink (returns ELOOP)
|
|
424
|
+
// Use this to prevent symlink attacks
|
|
425
|
+
int fd = open(path.c_str(), O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
|
|
426
|
+
if (fd < 0 && errno == ELOOP) {
|
|
427
|
+
// Path is a symlink - handle appropriately
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// O_SYMLINK: Open the symlink itself (not following it)
|
|
431
|
+
// Use this when you need to operate on the symlink
|
|
432
|
+
int fd = open(symlink_path.c_str(), O_RDONLY | O_SYMLINK | O_CLOEXEC);
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Reference**: [POSIX open()](https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html)
|
|
436
|
+
|
|
437
|
+
### fstat, fstatfs, fstatvfs (TOCTOU-Safe Variants)
|
|
438
|
+
|
|
439
|
+
**Purpose**: Get file/filesystem information via file descriptor to prevent TOCTOU races.
|
|
440
|
+
|
|
441
|
+
**Why File Descriptor Variants are Safer**:
|
|
442
|
+
|
|
443
|
+
The path-based functions (`stat()`, `statfs()`, `statvfs()`) are vulnerable to TOCTOU:
|
|
444
|
+
|
|
445
|
+
1. You call `stat("/path/to/file")`
|
|
446
|
+
2. Attacker replaces the file with a symlink to sensitive data
|
|
447
|
+
3. You operate on the wrong file
|
|
448
|
+
|
|
449
|
+
File descriptor variants operate on the already-opened inode, not the path.
|
|
450
|
+
|
|
451
|
+
**Usage Pattern**:
|
|
452
|
+
|
|
453
|
+
```cpp
|
|
454
|
+
// 1. Open with security flags
|
|
455
|
+
int fd = open(path.c_str(), O_RDONLY | O_DIRECTORY | O_CLOEXEC);
|
|
456
|
+
if (fd < 0) { /* handle error */ }
|
|
457
|
+
FdGuard guard(fd);
|
|
458
|
+
|
|
459
|
+
// 2. Use fd-based functions - these operate on the same inode
|
|
460
|
+
struct stat st;
|
|
461
|
+
if (fstat(fd, &st) != 0) { /* handle error */ }
|
|
462
|
+
|
|
463
|
+
struct statfs fs;
|
|
464
|
+
if (fstatfs(fd, &fs) != 0) { /* handle error */ }
|
|
465
|
+
|
|
466
|
+
struct statvfs vfs;
|
|
467
|
+
if (fstatvfs(fd, &vfs) != 0) { /* handle error */ }
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
**Function Comparison**:
|
|
471
|
+
|
|
472
|
+
| Path-based (TOCTOU risk) | FD-based (Safe) | Purpose |
|
|
473
|
+
| ------------------------ | --------------------- | ----------------------- |
|
|
474
|
+
| `stat(path, &st)` | `fstat(fd, &st)` | File metadata |
|
|
475
|
+
| `statfs(path, &fs)` | `fstatfs(fd, &fs)` | Filesystem info (macOS) |
|
|
476
|
+
| `statvfs(path, &vfs)` | `fstatvfs(fd, &vfs)` | Filesystem info (POSIX) |
|
|
477
|
+
| `chflags(path, flags)` | `fchflags(fd, flags)` | Set file flags |
|
|
478
|
+
|
|
479
|
+
**Reference**: [Apple Secure Coding Guide - Race Conditions](https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/RaceConditions.html)
|
|
265
480
|
|
|
266
481
|
## Security APIs
|
|
267
482
|
|
|
@@ -284,19 +499,75 @@ bool accessible = faccessat(AT_FDCWD, path.c_str(), R_OK, AT_EACCESS) == 0;
|
|
|
284
499
|
- Prevents TOCTOU (Time-of-Check-Time-of-Use) attacks
|
|
285
500
|
- More secure than deprecated `access()` function
|
|
286
501
|
|
|
287
|
-
### Path
|
|
502
|
+
### realpath() - Path Canonicalization
|
|
288
503
|
|
|
289
|
-
**Purpose**:
|
|
504
|
+
**Purpose**: Resolve symbolic links, eliminate `.` and `..` components, and validate path existence.
|
|
290
505
|
|
|
291
|
-
**
|
|
506
|
+
**Apple Documentation**: [realpath(3)](https://developer.apple.com/library/archive/documentation/System/Conceptual/ManPages_iPhoneOS/man3/realpath.3.html)
|
|
507
|
+
|
|
508
|
+
**CERT C Secure Coding**: [FIO02-C. Canonicalize path names](https://wiki.sei.cmu.edu/confluence/x/DtcxBQ)
|
|
509
|
+
|
|
510
|
+
**Why realpath() is Essential for Security**:
|
|
511
|
+
|
|
512
|
+
1. **Symlink Resolution**: Resolves all symbolic links to their targets
|
|
513
|
+
2. **Path Normalization**: Eliminates `.`, `..`, and redundant slashes
|
|
514
|
+
3. **Existence Validation**: Returns `NULL` if path doesn't exist
|
|
515
|
+
4. **Prevents Directory Traversal**: `../../../etc/passwd` becomes `/etc/passwd`
|
|
516
|
+
|
|
517
|
+
**Usage in Project**:
|
|
292
518
|
|
|
293
519
|
```cpp
|
|
294
|
-
//
|
|
295
|
-
|
|
296
|
-
|
|
520
|
+
#include <sys/param.h> // For PATH_MAX
|
|
521
|
+
#include <unistd.h> // For realpath()
|
|
522
|
+
|
|
523
|
+
std::string ValidateAndCanonicalizePath(const std::string& path, std::string& error) {
|
|
524
|
+
// Security check: Reject paths with null bytes (injection attack)
|
|
525
|
+
if (path.find('\0') != std::string::npos) {
|
|
526
|
+
error = "Invalid path containing null byte";
|
|
527
|
+
return "";
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Canonicalize path using realpath()
|
|
531
|
+
char resolved_path[PATH_MAX];
|
|
532
|
+
if (realpath(path.c_str(), resolved_path) != nullptr) {
|
|
533
|
+
return std::string(resolved_path);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// realpath() failed
|
|
537
|
+
int err = errno;
|
|
538
|
+
if (err == ENOENT) {
|
|
539
|
+
error = "Path does not exist: " + path;
|
|
540
|
+
} else if (err == EACCES) {
|
|
541
|
+
error = "Permission denied: " + path;
|
|
542
|
+
} else {
|
|
543
|
+
error = "Path validation failed: " + std::string(strerror(err));
|
|
544
|
+
}
|
|
545
|
+
return "";
|
|
297
546
|
}
|
|
547
|
+
```
|
|
548
|
+
|
|
549
|
+
**Important Notes**:
|
|
298
550
|
|
|
299
|
-
|
|
551
|
+
- `realpath()` is POSIX-standard, works on macOS and Linux
|
|
552
|
+
- The resolved path buffer must be at least `PATH_MAX` bytes
|
|
553
|
+
- Returns `NULL` on error, check `errno` for details
|
|
554
|
+
- **Limitation**: Still has TOCTOU risk between `realpath()` and subsequent operations - combine with fd-based APIs
|
|
555
|
+
|
|
556
|
+
**Error Codes**:
|
|
557
|
+
|
|
558
|
+
- `ENOENT` - Path component does not exist
|
|
559
|
+
- `EACCES` - Permission denied for a path component
|
|
560
|
+
- `ELOOP` - Too many symbolic links (possible symlink loop)
|
|
561
|
+
- `ENAMETOOLONG` - Resulting path exceeds `PATH_MAX`
|
|
562
|
+
|
|
563
|
+
### Path Validation (Legacy Approach)
|
|
564
|
+
|
|
565
|
+
**Note**: Prefer `realpath()` over manual validation - it's more comprehensive.
|
|
566
|
+
|
|
567
|
+
**Simple Null Byte Check** (still useful as first-line defense):
|
|
568
|
+
|
|
569
|
+
```cpp
|
|
570
|
+
// Check for null bytes (path injection attack)
|
|
300
571
|
if (path.find('\0') != std::string::npos) {
|
|
301
572
|
throw std::invalid_argument("Path cannot contain null bytes");
|
|
302
573
|
}
|
|
@@ -332,20 +603,61 @@ public:
|
|
|
332
603
|
|
|
333
604
|
### Memory Management Rules
|
|
334
605
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
- Always use CFReleaser for owned objects
|
|
606
|
+
**Apple's Official Ownership Documentation**: [Memory Management Programming Guide for Core Foundation](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Concepts/Ownership.html)
|
|
607
|
+
|
|
608
|
+
#### 1. Core Foundation Create/Copy/Get Rule
|
|
339
609
|
|
|
340
|
-
|
|
341
|
-
- Use RAII wrappers for malloc'd buffers
|
|
342
|
-
- Prefer stack allocation when size is known
|
|
343
|
-
- Use std::vector for dynamic arrays
|
|
610
|
+
This is the fundamental rule for Core Foundation memory management:
|
|
344
611
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
612
|
+
| Function Name Contains | You Own It? | Must Call CFRelease? |
|
|
613
|
+
| ---------------------- | ----------- | ----------------------- |
|
|
614
|
+
| **Create** | Yes | Yes |
|
|
615
|
+
| **Copy** | Yes | Yes |
|
|
616
|
+
| **Get** | No | No (borrowed reference) |
|
|
617
|
+
|
|
618
|
+
**Examples from DiskArbitration**:
|
|
619
|
+
|
|
620
|
+
```cpp
|
|
621
|
+
// DASessionCreate - you own it, must release
|
|
622
|
+
DASessionRef session = DASessionCreate(kCFAllocatorDefault);
|
|
623
|
+
// ... use session ...
|
|
624
|
+
CFRelease(session); // Required
|
|
625
|
+
|
|
626
|
+
// DADiskCopyDescription - you own it, must release
|
|
627
|
+
CFDictionaryRef desc = DADiskCopyDescription(disk);
|
|
628
|
+
// ... use desc ...
|
|
629
|
+
CFRelease(desc); // Required
|
|
630
|
+
|
|
631
|
+
// CFDictionaryGetValue - borrowed, do NOT release
|
|
632
|
+
CFStringRef name = (CFStringRef)CFDictionaryGetValue(desc, kDADiskDescriptionVolumeNameKey);
|
|
633
|
+
// ... use name ...
|
|
634
|
+
// NO CFRelease(name) - it's owned by the dictionary!
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
**Using RAII to Enforce the Rule**:
|
|
638
|
+
|
|
639
|
+
```cpp
|
|
640
|
+
// CFReleaser automatically releases Create/Copy results
|
|
641
|
+
CFReleaser<DASessionRef> session(DASessionCreate(kCFAllocatorDefault));
|
|
642
|
+
CFReleaser<CFDictionaryRef> desc(DADiskCopyDescription(disk.get()));
|
|
643
|
+
|
|
644
|
+
// Get results are NOT wrapped - they're borrowed
|
|
645
|
+
CFStringRef name = (CFStringRef)CFDictionaryGetValue(desc.get(), key);
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
#### 2. Buffer Management
|
|
649
|
+
|
|
650
|
+
- Use RAII wrappers for `malloc()`'d buffers (e.g., `getmntinfo_r_np()`)
|
|
651
|
+
- Prefer stack allocation when size is known at compile time
|
|
652
|
+
- Use `std::vector` for dynamic arrays in C++
|
|
653
|
+
- **Never mix `malloc()`/`free()` with `new`/`delete`**
|
|
654
|
+
|
|
655
|
+
#### 3. String Handling
|
|
656
|
+
|
|
657
|
+
- Convert `CFStringRef` to `std::string` early in the call chain
|
|
658
|
+
- Use `std::string` for all internal processing
|
|
659
|
+
- Convert back to `CFStringRef` only at API boundaries
|
|
660
|
+
- Use `CFStringGetCString()` with `kCFStringEncodingUTF8`
|
|
349
661
|
|
|
350
662
|
## Thread Safety Considerations
|
|
351
663
|
|
|
@@ -463,7 +775,31 @@ public:
|
|
|
463
775
|
|
|
464
776
|
## References
|
|
465
777
|
|
|
778
|
+
### Apple Official Documentation
|
|
779
|
+
|
|
466
780
|
- [Apple Developer Documentation](https://developer.apple.com/documentation/)
|
|
467
781
|
- [Core Foundation Programming Guide](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFDesignConcepts/CFDesignConcepts.html)
|
|
782
|
+
- [Core Foundation Memory Management](https://developer.apple.com/library/archive/documentation/CoreFoundation/Conceptual/CFMemoryMgmt/Concepts/Ownership.html)
|
|
468
783
|
- [DiskArbitration Programming Guide](https://developer.apple.com/library/archive/documentation/DriversKernelHardware/Conceptual/DiskArbitrationProgGuide/Introduction/Introduction.html)
|
|
784
|
+
- [DiskArbitration Framework Reference](https://developer.apple.com/documentation/diskarbitration)
|
|
469
785
|
- [File System Programming Guide](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/FileSystemProgrammingGuide/Introduction/Introduction.html)
|
|
786
|
+
- [Secure Coding Guide - Race Conditions](https://developer.apple.com/library/archive/documentation/Security/Conceptual/SecureCodingGuide/Articles/RaceConditions.html)
|
|
787
|
+
|
|
788
|
+
### Darwin/XNU Source Code
|
|
789
|
+
|
|
790
|
+
- [darwin-xnu fcntl.h](https://github.com/apple/darwin-xnu/blob/main/bsd/sys/fcntl.h) - Open flags (O_CLOEXEC, O_SYMLINK, etc.)
|
|
791
|
+
|
|
792
|
+
### BSD/FreeBSD Documentation (macOS derives from BSD)
|
|
793
|
+
|
|
794
|
+
- [fchflags(2)](https://man.freebsd.org/cgi/man.cgi?query=fchflags&sektion=2) - File descriptor-based flag modification
|
|
795
|
+
- [chflags(2)](https://man.freebsd.org/cgi/man.cgi?query=chflags&sektion=2) - BSD file flags
|
|
796
|
+
- [getmntinfo(3)](https://keith.github.io/xcode-man-pages/getmntinfo.3.html) - Mount information (includes `getmntinfo_r_np`)
|
|
797
|
+
|
|
798
|
+
### POSIX Standards
|
|
799
|
+
|
|
800
|
+
- [open()](https://pubs.opengroup.org/onlinepubs/9699919799/functions/open.html) - POSIX open specification
|
|
801
|
+
- [access()](https://man7.org/linux/man-pages/man2/faccessat.2.html) - faccessat documentation
|
|
802
|
+
|
|
803
|
+
### Security References
|
|
804
|
+
|
|
805
|
+
- [CERT C FIO02-C](https://wiki.sei.cmu.edu/confluence/x/DtcxBQ) - Canonicalize path names originating from tainted sources
|
|
@@ -86,14 +86,30 @@ if (GetVolumeNameForVolumeMountPointW(L"C:\\", volumeGUID, 50)) {
|
|
|
86
86
|
}
|
|
87
87
|
```
|
|
88
88
|
|
|
89
|
-
### FindFirstFileExA
|
|
89
|
+
### FindFirstFileExA / FindClose
|
|
90
90
|
|
|
91
|
-
- **Docs**:
|
|
91
|
+
- **Docs**:
|
|
92
|
+
- https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfileexa
|
|
93
|
+
- https://learn.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findclose
|
|
92
94
|
- **Purpose**: Searches a directory for files/subdirectories
|
|
93
95
|
- **Flags**:
|
|
94
96
|
- `FIND_FIRST_EX_LARGE_FETCH`: Optimize for large directories
|
|
95
97
|
- `FIND_FIRST_EX_ON_DISK_ENTRIES_ONLY`: Skip reparse points
|
|
96
98
|
- **Security**: Can follow symbolic links if not careful
|
|
99
|
+
- **Critical**: Search handles **must** be closed with `FindClose()`, not `CloseHandle()`
|
|
100
|
+
- **Return Value**: Returns `INVALID_HANDLE_VALUE` on failure (not `NULL`)
|
|
101
|
+
- **RAII Pattern**:
|
|
102
|
+
```cpp
|
|
103
|
+
class FindHandleGuard {
|
|
104
|
+
HANDLE handle;
|
|
105
|
+
public:
|
|
106
|
+
explicit FindHandleGuard(HANDLE h) : handle(h) {}
|
|
107
|
+
~FindHandleGuard() {
|
|
108
|
+
if (handle != INVALID_HANDLE_VALUE) FindClose(handle);
|
|
109
|
+
}
|
|
110
|
+
explicit operator bool() const { return handle != INVALID_HANDLE_VALUE; }
|
|
111
|
+
};
|
|
112
|
+
```
|
|
97
113
|
|
|
98
114
|
### GetFileAttributesW / SetFileAttributesW
|
|
99
115
|
|
|
@@ -221,11 +237,28 @@ if (GetVolumeNameForVolumeMountPointW(L"C:\\", volumeGUID, 50)) {
|
|
|
221
237
|
|
|
222
238
|
## Threading APIs
|
|
223
239
|
|
|
240
|
+
### CreateEvent
|
|
241
|
+
|
|
242
|
+
- **Docs**: https://learn.microsoft.com/en-us/windows/win32/api/synchapi/nf-synchapi-createeventa
|
|
243
|
+
- **Purpose**: Creates or opens a named or unnamed event object
|
|
244
|
+
- **Return Value**: Returns `NULL` on failure (not `INVALID_HANDLE_VALUE`)
|
|
245
|
+
- **Error Handling**: Always check return value; call `GetLastError()` on failure
|
|
246
|
+
- **Cleanup**: Use `CloseHandle()` when done
|
|
247
|
+
- **Pattern**:
|
|
248
|
+
```cpp
|
|
249
|
+
HANDLE event = CreateEvent(nullptr, FALSE, FALSE, nullptr);
|
|
250
|
+
if (event == NULL) {
|
|
251
|
+
DWORD error = GetLastError();
|
|
252
|
+
throw std::runtime_error("CreateEvent failed: " + std::to_string(error));
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
224
256
|
### CreateThread
|
|
225
257
|
|
|
226
258
|
- **Docs**: https://docs.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-createthread
|
|
227
259
|
- **Best Practice**: Always store handle for cleanup
|
|
228
260
|
- **Never**: Never use `TerminateThread` - it can corrupt process state
|
|
261
|
+
- **Never**: Never use `std::thread::detach()` for timeout handling - use `std::future::wait_for()` instead
|
|
229
262
|
|
|
230
263
|
### WaitForSingleObject / WaitForMultipleObjects
|
|
231
264
|
|
package/doc/gotchas.md
CHANGED
|
@@ -4,6 +4,34 @@ This guide covers common issues, platform quirks, and important considerations w
|
|
|
4
4
|
|
|
5
5
|
## Timeout Issues
|
|
6
6
|
|
|
7
|
+
### Configuring the Default Timeout
|
|
8
|
+
|
|
9
|
+
The default timeout is 5000ms (5 seconds). You can override it in two ways:
|
|
10
|
+
|
|
11
|
+
**1. Environment variable** (applies globally):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Linux/macOS
|
|
15
|
+
export FS_METADATA_TIMEOUT_MS=30000
|
|
16
|
+
|
|
17
|
+
# Windows
|
|
18
|
+
set FS_METADATA_TIMEOUT_MS=30000
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
**2. Per-call options** (takes precedence):
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
const metadata = await getVolumeMetadata("/mnt/nas", {
|
|
25
|
+
timeoutMs: 30000, // 30 seconds
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
The environment variable is useful for:
|
|
30
|
+
|
|
31
|
+
- CI/CD pipelines with slow or emulated environments
|
|
32
|
+
- Docker containers accessing remote volumes
|
|
33
|
+
- Systems with many network mounts
|
|
34
|
+
|
|
7
35
|
### Network Volumes Can Hang
|
|
8
36
|
|
|
9
37
|
**Problem**: Linux, macOS, and Windows can block system calls indefinitely when network filesystems are unhealthy.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@photostructure/fs-metadata",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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",
|
|
@@ -70,8 +70,8 @@
|
|
|
70
70
|
"requireBranch": "main",
|
|
71
71
|
"commitMessage": "release: ${version}",
|
|
72
72
|
"tagName": "v${version}",
|
|
73
|
-
"commitArgs": "-
|
|
74
|
-
"tagArgs": "
|
|
73
|
+
"commitArgs": "--gpg-sign",
|
|
74
|
+
"tagArgs": "--sign"
|
|
75
75
|
},
|
|
76
76
|
"github": {
|
|
77
77
|
"release": true
|
|
@@ -113,34 +113,31 @@
|
|
|
113
113
|
"node-gyp-build": "^4.8.4"
|
|
114
114
|
},
|
|
115
115
|
"devDependencies": {
|
|
116
|
-
"@eslint/js": "^9.38.0",
|
|
117
116
|
"@types/jest": "^30.0.0",
|
|
118
|
-
"@types/node": "^24.
|
|
117
|
+
"@types/node": "^24.10.1",
|
|
119
118
|
"@types/semver": "^7.7.1",
|
|
120
|
-
"@typescript-eslint/eslint-plugin": "^8.46.2",
|
|
121
|
-
"@typescript-eslint/parser": "^8.46.2",
|
|
122
119
|
"cross-env": "^10.1.0",
|
|
123
120
|
"del-cli": "^7.0.0",
|
|
124
|
-
"eslint": "
|
|
121
|
+
"eslint": "9.39.1",
|
|
125
122
|
"eslint-plugin-regexp": "^2.10.0",
|
|
126
123
|
"eslint-plugin-security": "^3.0.1",
|
|
127
|
-
"globals": "^16.
|
|
124
|
+
"globals": "^16.5.0",
|
|
128
125
|
"jest": "^30.2.0",
|
|
129
126
|
"jest-environment-node": "^30.2.0",
|
|
130
|
-
"jest-extended": "^
|
|
131
|
-
"node-gyp": "^
|
|
132
|
-
"npm-check-updates": "^19.1.
|
|
127
|
+
"jest-extended": "^7.0.0",
|
|
128
|
+
"node-gyp": "^12.1.0",
|
|
129
|
+
"npm-check-updates": "^19.1.2",
|
|
133
130
|
"npm-run-all": "4.1.5",
|
|
134
131
|
"prebuildify": "^6.0.1",
|
|
135
|
-
"prettier": "^3.
|
|
132
|
+
"prettier": "^3.7.3",
|
|
136
133
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
137
|
-
"release-it": "^19.0.
|
|
138
|
-
"terser": "^5.44.
|
|
139
|
-
"ts-jest": "^29.4.
|
|
140
|
-
"tsup": "^8.5.
|
|
141
|
-
"tsx": "^4.
|
|
142
|
-
"typedoc": "^0.28.
|
|
134
|
+
"release-it": "^19.0.6",
|
|
135
|
+
"terser": "^5.44.1",
|
|
136
|
+
"ts-jest": "^29.4.6",
|
|
137
|
+
"tsup": "^8.5.1",
|
|
138
|
+
"tsx": "^4.21.0",
|
|
139
|
+
"typedoc": "^0.28.15",
|
|
143
140
|
"typescript": "^5.9.3",
|
|
144
|
-
"typescript-eslint": "^8.
|
|
141
|
+
"typescript-eslint": "^8.48.0"
|
|
145
142
|
}
|
|
146
143
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/scripts/precommit.ts
CHANGED
|
@@ -27,7 +27,10 @@ function run({
|
|
|
27
27
|
run({ cmd: "npm install", desc: "Installing dependencies" });
|
|
28
28
|
run({ cmd: "npm run update", desc: "Updating dependencies" });
|
|
29
29
|
rmSync("package-lock.json", { force: true });
|
|
30
|
-
run({
|
|
30
|
+
run({
|
|
31
|
+
cmd: "npm install --ignore-scripts=false",
|
|
32
|
+
desc: "Updating dependencies",
|
|
33
|
+
});
|
|
31
34
|
run({ cmd: "npm run clean", desc: "Start fresh" });
|
|
32
35
|
run({ cmd: "npm run fmt", desc: "Formatting code" });
|
|
33
36
|
run({ cmd: "npm run lint", desc: "Running linting checks" });
|