@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.
Files changed (45) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/CLAUDE.md +1 -1
  3. package/CONTRIBUTING.md +15 -0
  4. package/README.md +2 -1
  5. package/dist/index.cjs +11 -4
  6. package/dist/index.cjs.map +1 -1
  7. package/dist/index.d.cts +10 -5
  8. package/dist/index.d.mts +10 -5
  9. package/dist/index.d.ts +10 -5
  10. package/dist/index.mjs +10 -3
  11. package/dist/index.mjs.map +1 -1
  12. package/doc/LINUX_API_REFERENCE.md +310 -0
  13. package/doc/MACOS_API_REFERENCE.md +367 -31
  14. package/doc/WINDOWS_API_REFERENCE.md +35 -2
  15. package/doc/gotchas.md +28 -0
  16. package/package.json +17 -20
  17. package/prebuilds/darwin-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  18. package/prebuilds/darwin-x64/@photostructure+fs-metadata.glibc.node +0 -0
  19. package/prebuilds/linux-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  20. package/prebuilds/linux-arm64/@photostructure+fs-metadata.musl.node +0 -0
  21. package/prebuilds/linux-x64/@photostructure+fs-metadata.glibc.node +0 -0
  22. package/prebuilds/linux-x64/@photostructure+fs-metadata.musl.node +0 -0
  23. package/prebuilds/win32-arm64/@photostructure+fs-metadata.glibc.node +0 -0
  24. package/prebuilds/win32-x64/@photostructure+fs-metadata.glibc.node +0 -0
  25. package/scripts/precommit.ts +4 -1
  26. package/src/common/fd_guard.h +71 -0
  27. package/src/{darwin → common}/path_security.h +8 -5
  28. package/src/common/volume_utils.h +51 -0
  29. package/src/darwin/hidden.cpp +47 -14
  30. package/src/darwin/raii_utils.h +8 -8
  31. package/src/darwin/volume_metadata.cpp +33 -39
  32. package/src/index.ts +3 -3
  33. package/src/linux/blkid_cache.cpp +5 -11
  34. package/src/linux/blkid_cache.h +21 -0
  35. package/src/linux/gio_utils.cpp +7 -23
  36. package/src/linux/gio_utils.h +16 -40
  37. package/src/linux/gio_volume_metadata.cpp +16 -88
  38. package/src/linux/volume_metadata.cpp +35 -27
  39. package/src/options.ts +16 -3
  40. package/src/types/options.ts +1 -1
  41. package/src/windows/drive_status.h +74 -49
  42. package/src/windows/error_utils.h +2 -2
  43. package/src/windows/security_utils.h +47 -2
  44. package/src/windows/thread_pool.h +29 -4
  45. 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
- - No need for run loop scheduling in synchronous operations
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
- **Usage in Project**:
323
+ **IMPORTANT**: Prefer `fchflags()` over `chflags()` to prevent TOCTOU race conditions.
324
+
325
+ **Secure Usage (TOCTOU-safe)**:
235
326
 
236
327
  ```cpp
237
- // Get current flags
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 (stat(path_.c_str(), &st) != 0) {
240
- SetError(CreateDetailedErrorMessage("stat", errno));
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 (chflags(path_.c_str(), flags) != 0) {
254
- SetError(CreateDetailedErrorMessage("chflags", errno));
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
- - `SF_ARCHIVED` - File has been archived
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 Validation
502
+ ### realpath() - Path Canonicalization
288
503
 
289
- **Purpose**: Prevent directory traversal and null byte injection attacks.
504
+ **Purpose**: Resolve symbolic links, eliminate `.` and `..` components, and validate path existence.
290
505
 
291
- **Implementation**:
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
- // Check for directory traversal
295
- if (path.find("..") != std::string::npos) {
296
- throw std::invalid_argument("Path cannot contain '..'");
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
- // Check for null bytes
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
- 1. **Core Foundation Create/Copy/Get Rule**:
336
- - Functions with "Create" or "Copy" return owned objects (must release)
337
- - Functions with "Get" return borrowed references (don't release)
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
- 2. **Buffer Management**:
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
- 3. **String Handling**:
346
- - Convert CFString to std::string early
347
- - Use std::string for all internal processing
348
- - Convert back to CFString only when needed
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**: https://docs.microsoft.com/en-us/windows/win32/api/fileapi/nf-fileapi-findfirstfileexa
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.7.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": "-S",
74
- "tagArgs": "-S"
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.9.1",
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": "^9.38.0",
121
+ "eslint": "9.39.1",
125
122
  "eslint-plugin-regexp": "^2.10.0",
126
123
  "eslint-plugin-security": "^3.0.1",
127
- "globals": "^16.4.0",
124
+ "globals": "^16.5.0",
128
125
  "jest": "^30.2.0",
129
126
  "jest-environment-node": "^30.2.0",
130
- "jest-extended": "^6.0.0",
131
- "node-gyp": "^11.5.0",
132
- "npm-check-updates": "^19.1.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.6.2",
132
+ "prettier": "^3.7.3",
136
133
  "prettier-plugin-organize-imports": "4.3.0",
137
- "release-it": "^19.0.5",
138
- "terser": "^5.44.0",
139
- "ts-jest": "^29.4.5",
140
- "tsup": "^8.5.0",
141
- "tsx": "^4.20.6",
142
- "typedoc": "^0.28.14",
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.46.2"
141
+ "typescript-eslint": "^8.48.0"
145
142
  }
146
143
  }
@@ -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({ cmd: "npm install", desc: "Updating dependencies" });
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" });