@openclaw/fs-safe 0.11.0 → 0.12.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 (116) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +4 -2
  3. package/dist/advanced.d.ts +1 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -1
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +4 -3
  8. package/dist/archive-gzip-tail.d.ts.map +1 -1
  9. package/dist/archive-gzip-tail.js +13 -9
  10. package/dist/archive-read.d.ts.map +1 -1
  11. package/dist/archive-read.js +37 -33
  12. package/dist/archive-zip-names.d.ts.map +1 -1
  13. package/dist/archive-zip-names.js +17 -5
  14. package/dist/bounded-read.js +2 -2
  15. package/dist/device-path.d.ts.map +1 -1
  16. package/dist/device-path.js +5 -3
  17. package/dist/file-hash.d.ts.map +1 -1
  18. package/dist/file-hash.js +9 -2
  19. package/dist/file-store-prune.d.ts.map +1 -1
  20. package/dist/file-store-prune.js +9 -1
  21. package/dist/filename.d.ts +1 -0
  22. package/dist/filename.d.ts.map +1 -1
  23. package/dist/filename.js +30 -13
  24. package/dist/guarded-mkdir.d.ts +2 -0
  25. package/dist/guarded-mkdir.d.ts.map +1 -1
  26. package/dist/guarded-mkdir.js +50 -15
  27. package/dist/guest.d.ts.map +1 -1
  28. package/dist/guest.js +12 -4
  29. package/dist/install-path.d.ts +6 -0
  30. package/dist/install-path.d.ts.map +1 -1
  31. package/dist/install-path.js +13 -0
  32. package/dist/json-durable-queue-ownership.d.ts +2 -0
  33. package/dist/json-durable-queue-ownership.d.ts.map +1 -1
  34. package/dist/json-durable-queue-ownership.js +47 -3
  35. package/dist/json-durable-queue-read.d.ts +6 -0
  36. package/dist/json-durable-queue-read.d.ts.map +1 -0
  37. package/dist/json-durable-queue-read.js +59 -0
  38. package/dist/json-durable-queue.d.ts +1 -1
  39. package/dist/json-durable-queue.d.ts.map +1 -1
  40. package/dist/json-durable-queue.js +37 -80
  41. package/dist/native-binding.d.ts +7 -0
  42. package/dist/native-binding.d.ts.map +1 -1
  43. package/dist/path-scope-lexical.d.ts +14 -0
  44. package/dist/path-scope-lexical.d.ts.map +1 -0
  45. package/dist/path-scope-lexical.js +27 -0
  46. package/dist/path.d.ts.map +1 -1
  47. package/dist/path.js +20 -2
  48. package/dist/pinned-write.js +1 -0
  49. package/dist/root-boundary.d.ts +25 -0
  50. package/dist/root-boundary.d.ts.map +1 -0
  51. package/dist/root-boundary.js +177 -0
  52. package/dist/root-context.d.ts.map +1 -1
  53. package/dist/root-context.js +32 -5
  54. package/dist/root-directory-list.d.ts.map +1 -1
  55. package/dist/root-directory-list.js +10 -2
  56. package/dist/root-impl.d.ts.map +1 -1
  57. package/dist/root-impl.js +53 -34
  58. package/dist/root-move-preflight.d.ts +8 -0
  59. package/dist/root-move-preflight.d.ts.map +1 -0
  60. package/dist/root-move-preflight.js +16 -0
  61. package/dist/root-path-existing.d.ts +2 -0
  62. package/dist/root-path-existing.d.ts.map +1 -1
  63. package/dist/root-path-existing.js +10 -2
  64. package/dist/root-path.d.ts +2 -0
  65. package/dist/root-path.d.ts.map +1 -1
  66. package/dist/root-path.js +54 -26
  67. package/dist/root-paths.d.ts +2 -6
  68. package/dist/root-paths.d.ts.map +1 -1
  69. package/dist/root-paths.js +14 -26
  70. package/dist/root-walk.d.ts.map +1 -1
  71. package/dist/root-walk.js +2 -1
  72. package/dist/root-write-mode.d.ts +2 -0
  73. package/dist/root-write-mode.d.ts.map +1 -1
  74. package/dist/root-write-mode.js +20 -7
  75. package/dist/root-write-verification.d.ts.map +1 -1
  76. package/dist/root-write-verification.js +12 -3
  77. package/dist/safe-path-segment.d.ts.map +1 -1
  78. package/dist/safe-path-segment.js +3 -1
  79. package/dist/secure-file-windows.d.ts +10 -0
  80. package/dist/secure-file-windows.d.ts.map +1 -0
  81. package/dist/secure-file-windows.js +186 -0
  82. package/dist/secure-file.d.ts.map +1 -1
  83. package/dist/secure-file.js +6 -3
  84. package/dist/sibling-staged-file.d.ts +1 -0
  85. package/dist/sibling-staged-file.d.ts.map +1 -1
  86. package/dist/sibling-staged-file.js +7 -0
  87. package/dist/sibling-temp.d.ts.map +1 -1
  88. package/dist/sibling-temp.js +2 -1
  89. package/dist/sidecar-lock-acquire.js +1 -1
  90. package/dist/sidecar-lock.d.ts.map +1 -1
  91. package/dist/sidecar-lock.js +2 -3
  92. package/dist/temp-target.d.ts.map +1 -1
  93. package/dist/temp-target.js +3 -1
  94. package/dist/walk.d.ts.map +1 -1
  95. package/dist/walk.js +11 -10
  96. package/docs/advanced.md +3 -1
  97. package/docs/archive.md +6 -0
  98. package/docs/contributing.md +4 -1
  99. package/docs/copy.md +1 -1
  100. package/docs/durability.md +4 -2
  101. package/docs/file-store.md +6 -0
  102. package/docs/filename.md +9 -2
  103. package/docs/guest.md +5 -0
  104. package/docs/install-path.md +59 -13
  105. package/docs/install.md +4 -1
  106. package/docs/native-helper.md +4 -2
  107. package/docs/native.md +14 -2
  108. package/docs/path.md +1 -1
  109. package/docs/permissions.md +26 -1
  110. package/docs/public-api.md +5 -0
  111. package/docs/root.md +3 -1
  112. package/docs/secure-file.md +15 -14
  113. package/docs/security-model.md +1 -1
  114. package/docs/store.md +22 -1
  115. package/docs/temp.md +4 -0
  116. package/package.json +8 -8
@@ -1 +1 @@
1
- {"version":3,"file":"root-write-mode.d.ts","sourceRoot":"","sources":["../src/root-write-mode.ts"],"names":[],"mappings":"AAUA,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAsClB"}
1
+ {"version":3,"file":"root-write-mode.d.ts","sourceRoot":"","sources":["../src/root-write-mode.ts"],"names":[],"mappings":"AAQA,OAAO,EAAuB,KAAK,oBAAoB,EAAE,MAAM,oBAAoB,CAAC;AAGpF,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,oBAAoB,CAAC;IACpC,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB,GAAG,OAAO,CAAC,MAAM,CAAC,CAkDlB"}
@@ -1,26 +1,33 @@
1
1
  import fsSync from "node:fs";
2
2
  import fs from "node:fs/promises";
3
3
  import { FsSafeError } from "./errors.js";
4
- import { isNotFoundPathError, isPathInside } from "./path.js";
4
+ import { isNotFoundPathError } from "./path.js";
5
5
  import { resolveReadOpenFlags } from "./read-open-flags.js";
6
6
  import { realpathSync } from "./realpath.js";
7
7
  import { hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
8
8
  import { inspectFileIdentity } from "./strict-file-identity.js";
9
+ import { admitPathInsideRoot } from "./root-boundary.js";
9
10
  // The caller has already resolved and guarded this write target.
10
11
  export async function inheritWriteTargetMode(params) {
11
12
  try {
12
- const existing = await inspectFileIdentity(() => fsSync.lstatSync(params.targetPath, { bigint: true }));
13
+ const admittedTarget = admitPathInsideRoot({
14
+ rootPath: params.rootWithSep,
15
+ candidatePath: params.targetPath,
16
+ rootIdentity: params.rootIdentity,
17
+ });
18
+ if (!admittedTarget)
19
+ throw outsideWorkspaceError();
20
+ const targetPath = admittedTarget.path;
21
+ const existing = await inspectFileIdentity(() => fsSync.lstatSync(targetPath, { bigint: true }));
13
22
  if (existing.isSymbolicLink())
14
23
  throw new FsSafeError("path-alias", "path alias escape blocked");
15
24
  if (!existing.isFile())
16
25
  throw new FsSafeError("not-file", "not a file");
17
26
  if (existing.nlink > 1n)
18
27
  throw hardlinkedPathNotAllowedError();
19
- if (!isPathInside(params.rootWithSep, params.targetPath))
20
- throw outsideWorkspaceError();
21
28
  // Preserve read-open admission of the pre-existing destination. access(2)
22
29
  // is not equivalent: it ignores ACLs on Windows and capabilities on Linux.
23
- const handle = await fs.open(params.targetPath, resolveReadOpenFlags());
30
+ const handle = await fs.open(targetPath, resolveReadOpenFlags());
24
31
  try {
25
32
  // Bind admission to the inode whose metadata is inherited.
26
33
  await inspectFileIdentity(() => fsSync.fstatSync(handle.fd, { bigint: true }), existing);
@@ -31,9 +38,15 @@ export async function inheritWriteTargetMode(params) {
31
38
  try {
32
39
  // A parent can change after guarded resolution. Do not inherit metadata
33
40
  // from an outside inode, even if the parent is restored before publication.
34
- const realPath = realpathSync.native(params.targetPath);
35
- if (!isPathInside(params.rootWithSep, realPath))
41
+ const observedRealPath = realpathSync.native(targetPath);
42
+ const admittedRealPath = admitPathInsideRoot({
43
+ rootPath: params.rootWithSep,
44
+ candidatePath: observedRealPath,
45
+ rootIdentity: params.rootIdentity,
46
+ });
47
+ if (!admittedRealPath)
36
48
  throw outsideWorkspaceError();
49
+ const realPath = admittedRealPath.path;
37
50
  await inspectFileIdentity(async () => {
38
51
  const current = fsSync.statSync(realPath, { bigint: true });
39
52
  if (!current.isFile())
@@ -1 +1 @@
1
- {"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAGhF,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;CACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CAqFhB"}
1
+ {"version":3,"file":"root-write-verification.d.ts","sourceRoot":"","sources":["../src/root-write-verification.ts"],"names":[],"mappings":"AAEA,OAAO,EAA6B,KAAK,sBAAsB,EAAE,MAAM,sBAAsB,CAAC;AAK9F,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,mBAAmB,CAAC;AAEhE,OAAO,EAA6B,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAIhF,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,IAAI,EAAE,WAAW,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,gBAAgB,EAAE,sBAAsB,CAAC;IACzC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,sBAAsB,CAAC;CACrC,GAAG,OAAO,CAAC,IAAI,CAAC,CA6FhB"}
@@ -4,10 +4,11 @@ import { assertAsyncDirectoryGuard } from "./directory-guard.js";
4
4
  import { FsSafeError } from "./errors.js";
5
5
  import { sameFileIdentity } from "./file-identity.js";
6
6
  import { resolveOpenedFileRealPathForFd } from "./opened-realpath.js";
7
- import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isPathInside, isSymlinkOpenError } from "./path.js";
7
+ import { assertNoUnsafeDeviceReadPath, hasNodeErrorCode, isNotFoundPathError, isSymlinkOpenError } from "./path.js";
8
8
  import { resolveReadOpenFlags } from "./read-open-flags.js";
9
9
  import { assertRootIdentityCurrent } from "./root-context.js";
10
10
  import { fileNotFoundError, hardlinkedPathNotAllowedError, outsideWorkspaceError } from "./root-errors.js";
11
+ import { admitPathInsideRoot } from "./root-boundary.js";
11
12
  export async function verifyAtomicWriteResult(params) {
12
13
  let needsPathOpen = false;
13
14
  const assertFile = (stat) => {
@@ -49,7 +50,11 @@ export async function verifyAtomicWriteResult(params) {
49
50
  assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
50
51
  const { realPath } = await resolveOpenedFileRealPathForFd(params.fd, stat, params.targetPath);
51
52
  assertPath(fsSync.statSync(realPath, { bigint: true }));
52
- if (!isPathInside(params.root.rootWithSep, realPath)) {
53
+ if (!admitPathInsideRoot({
54
+ rootPath: params.root.rootReal,
55
+ candidatePath: realPath,
56
+ rootIdentity: params.root.rootIdentity,
57
+ })) {
53
58
  throw outsideWorkspaceError();
54
59
  }
55
60
  await assertAsyncDirectoryGuard(params.parentGuard);
@@ -76,7 +81,11 @@ export async function verifyAtomicWriteResult(params) {
76
81
  assertPath(fsSync.lstatSync(params.targetPath, { bigint: true }));
77
82
  const { realPath: reopenedPath } = await resolveOpenedFileRealPathForFd(opened.fd, reopenedStat, params.targetPath);
78
83
  assertPath(fsSync.statSync(reopenedPath, { bigint: true }));
79
- if (!isPathInside(params.root.rootWithSep, reopenedPath)) {
84
+ if (!admitPathInsideRoot({
85
+ rootPath: params.root.rootReal,
86
+ candidatePath: reopenedPath,
87
+ rootIdentity: params.root.rootIdentity,
88
+ })) {
80
89
  throw outsideWorkspaceError();
81
90
  }
82
91
  await assertAsyncDirectoryGuard(params.parentGuard);
@@ -1 +1 @@
1
- {"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAaT;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAUR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAeR"}
1
+ {"version":3,"file":"safe-path-segment.d.ts","sourceRoot":"","sources":["../src/safe-path-segment.ts"],"names":[],"mappings":"AAWA,MAAM,MAAM,sBAAsB,GAAG;IACnC,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,iCAAiC,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,CAKtF;AAED,wBAAgB,eAAe,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAUrD;AAED,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,OAAO,CAaT;AAED,wBAAgB,qBAAqB,CACnC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAUR;AAED,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAWR;AAED,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,MAAM,EACd,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAeR"}
@@ -4,12 +4,14 @@ const SAFE_DOT_PREFIX_PATH_SEGMENT_PATTERN = /^[A-Za-z0-9._-]+$/;
4
4
  // Windows treats "C:name" as relative to the drive's current directory even
5
5
  // though path.win32.isAbsolute() reports false.
6
6
  const DRIVE_RELATIVE_PREFIX = /^[A-Za-z]:(?![\\/])/;
7
+ // A forward slash ends the segment, so "C:/file" still contains the "C:" segment.
8
+ const DRIVE_RELATIVE_SEGMENT = /(?:^|\/)[A-Za-z]:(?!\\)/;
7
9
  const HYPHEN_CHAR_CODE = 0x2d;
8
10
  export function isDriveRelativePath(value) {
9
11
  return DRIVE_RELATIVE_PREFIX.test(value);
10
12
  }
11
13
  export function assertNoDriveRelativePathSegments(value, label) {
12
- if (value.split("/").some(isDriveRelativePath)) {
14
+ if (DRIVE_RELATIVE_SEGMENT.test(value)) {
13
15
  throw new FsSafeError("invalid-path", `${label} must not contain a drive letter`);
14
16
  }
15
17
  return value;
@@ -0,0 +1,10 @@
1
+ import type { BigIntStats, Stats } from "node:fs";
2
+ import type { PermissionCheck } from "./permissions.js";
3
+ type ExactIdentity = Pick<BigIntStats, "dev" | "ino">;
4
+ export declare function inspectSecureWindowsDescriptor(params: {
5
+ fd: number;
6
+ identity: ExactIdentity;
7
+ stat: Stats;
8
+ }): PermissionCheck;
9
+ export {};
10
+ //# sourceMappingURL=secure-file-windows.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"secure-file-windows.d.ts","sourceRoot":"","sources":["../src/secure-file-windows.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AASlD,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AA4BxD,KAAK,aAAa,GAAG,IAAI,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC;AAuHtD,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,EAAE,aAAa,CAAC;IACxB,IAAI,EAAE,KAAK,CAAC;CACb,GAAG,eAAe,CA0ClB"}
@@ -0,0 +1,186 @@
1
+ import { FsSafeError } from "./errors.js";
2
+ import { recordFileObservationFailure } from "./file-observation.js";
3
+ import { getNativeBinding } from "./native.js";
4
+ const IDENTITY_RE = /^([0-9a-f]{8}):([0-9a-f]{16})$/;
5
+ const SID_RE = /^s-\d+-\d+(?:-\d+)+$/;
6
+ const OWNER_CLASSES = new Set(["current-user", "system", "administrators", "foreign"]);
7
+ const TRUSTED_OWNER_CLASSES = new Set(["current-user", "system", "administrators"]);
8
+ const SYSTEM_SID = "s-1-5-18";
9
+ const ADMINISTRATORS_SID = "s-1-5-32-544";
10
+ const WORLD_SIDS = new Set([
11
+ "s-1-1-0", "s-1-5-11", "s-1-5-32-545", "s-1-5-7",
12
+ "s-1-5-32-546", "s-1-5-4", "s-1-5-2",
13
+ ]);
14
+ const GENERIC_READ = 0x8000_0000;
15
+ const GENERIC_WRITE = 0x4000_0000;
16
+ const GENERIC_ALL = 0x1000_0000;
17
+ const DELETE_ACCESS = 0x0001_0000;
18
+ const WRITE_DAC = 0x0004_0000;
19
+ const WRITE_OWNER = 0x0008_0000;
20
+ const FILE_READ_DATA = 0x0000_0001;
21
+ const FILE_WRITE_DATA = 0x0000_0002;
22
+ const FILE_APPEND_DATA = 0x0000_0004;
23
+ const FILE_READ_EA = 0x0000_0008;
24
+ const FILE_WRITE_EA = 0x0000_0010;
25
+ const FILE_DELETE_CHILD = 0x0000_0040;
26
+ const FILE_READ_ATTRIBUTES = 0x0000_0080;
27
+ const FILE_WRITE_ATTRIBUTES = 0x0000_0100;
28
+ function permissionUnverified(message, cause) {
29
+ throw new FsSafeError("permission-unverified", message, cause === undefined ? {} : { cause });
30
+ }
31
+ function identityMismatch() {
32
+ const error = new FsSafeError("path-mismatch", "file identity changed or could not be verified");
33
+ recordFileObservationFailure(error, "identity");
34
+ throw error;
35
+ }
36
+ function isRecord(value) {
37
+ return typeof value === "object" && value !== null;
38
+ }
39
+ function isUint32(value) {
40
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 0xffff_ffff;
41
+ }
42
+ function parseIdentity(value) {
43
+ if (typeof value !== "string")
44
+ identityMismatch();
45
+ const match = IDENTITY_RE.exec(value);
46
+ if (!match)
47
+ identityMismatch();
48
+ return {
49
+ dev: BigInt(`0x${match[1]}`),
50
+ ino: BigInt(`0x${match[2]}`),
51
+ };
52
+ }
53
+ function validateFlags(value) {
54
+ if (!isRecord(value))
55
+ return false;
56
+ const raw = value.raw;
57
+ if (!isUint32(raw) || (raw & ~0xdf) !== 0)
58
+ return false;
59
+ const fields = [
60
+ ["objectInherit", 0x01], ["containerInherit", 0x02],
61
+ ["noPropagateInherit", 0x04], ["inheritOnly", 0x08], ["inherited", 0x10],
62
+ ["successfulAccess", 0x40], ["failedAccess", 0x80],
63
+ ];
64
+ return fields.every(([field, bit]) => typeof value[field] === "boolean" && value[field] === ((raw & bit) !== 0));
65
+ }
66
+ function validateAce(value) {
67
+ return isRecord(value) && typeof value.sid === "string" && SID_RE.test(value.sid) &&
68
+ isUint32(value.mask) && (value.aceType === "allow" || value.aceType === "deny") &&
69
+ validateFlags(value.flags);
70
+ }
71
+ function canRead(mask) {
72
+ return (mask & (GENERIC_ALL | GENERIC_READ | FILE_READ_DATA | FILE_READ_EA |
73
+ FILE_READ_ATTRIBUTES)) !== 0;
74
+ }
75
+ function canWrite(mask) {
76
+ return (mask & (GENERIC_ALL | GENERIC_WRITE | FILE_WRITE_DATA | FILE_APPEND_DATA |
77
+ FILE_WRITE_EA | FILE_WRITE_ATTRIBUTES | FILE_DELETE_CHILD | DELETE_ACCESS |
78
+ WRITE_DAC | WRITE_OWNER)) !== 0;
79
+ }
80
+ function expectedOwnerClass(facts) {
81
+ if (facts.ownerSid === facts.currentUserSid)
82
+ return "current-user";
83
+ if (facts.ownerSid === SYSTEM_SID)
84
+ return "system";
85
+ if (facts.ownerSid === ADMINISTRATORS_SID)
86
+ return "administrators";
87
+ return "foreign";
88
+ }
89
+ function validateSecurity(value) {
90
+ if (!isRecord(value))
91
+ permissionUnverified("Windows descriptor ACL facts were malformed");
92
+ const facts = value;
93
+ const booleans = [
94
+ "worldWritable", "groupWritable", "worldReadable", "groupReadable",
95
+ "fallbackRequired", "daclPresent", "isLocal", "aceListComplete",
96
+ ];
97
+ if (!booleans.every((field) => typeof facts[field] === "boolean") ||
98
+ typeof facts.ownerSid !== "string" || !SID_RE.test(facts.ownerSid) ||
99
+ typeof facts.currentUserSid !== "string" || !SID_RE.test(facts.currentUserSid) ||
100
+ typeof facts.ownerClass !== "string" || !OWNER_CLASSES.has(facts.ownerClass) ||
101
+ !Array.isArray(facts.unsupportedAceTypes) ||
102
+ !facts.unsupportedAceTypes.every(isUint32) ||
103
+ !Array.isArray(facts.aces) || !facts.aces.every(validateAce)) {
104
+ permissionUnverified("Windows descriptor ACL facts were malformed");
105
+ }
106
+ if (facts.ownerClass !== expectedOwnerClass(facts)) {
107
+ permissionUnverified("Windows descriptor owner facts were inconsistent");
108
+ }
109
+ if (facts.fallbackRequired || !facts.isLocal || !facts.aceListComplete ||
110
+ facts.unsupportedAceTypes.length !== 0) {
111
+ permissionUnverified("Windows descriptor ACL facts were incomplete or unsupported");
112
+ }
113
+ if (!facts.daclPresent && facts.aces.length !== 0) {
114
+ permissionUnverified("Windows descriptor DACL facts were inconsistent");
115
+ }
116
+ return facts;
117
+ }
118
+ function validatePermissionSummary(facts) {
119
+ let worldReadable = !facts.daclPresent;
120
+ let worldWritable = !facts.daclPresent;
121
+ let groupReadable = false;
122
+ let groupWritable = false;
123
+ const trustedSids = new Set([facts.currentUserSid, SYSTEM_SID, ADMINISTRATORS_SID]);
124
+ for (const ace of facts.aces) {
125
+ if (ace.aceType !== "allow" || ace.flags.inheritOnly || trustedSids.has(ace.sid))
126
+ continue;
127
+ const world = WORLD_SIDS.has(ace.sid);
128
+ if (world) {
129
+ worldReadable ||= canRead(ace.mask);
130
+ worldWritable ||= canWrite(ace.mask);
131
+ }
132
+ else {
133
+ groupReadable ||= canRead(ace.mask);
134
+ groupWritable ||= canWrite(ace.mask);
135
+ }
136
+ }
137
+ if (facts.worldReadable !== worldReadable || facts.worldWritable !== worldWritable ||
138
+ facts.groupReadable !== groupReadable || facts.groupWritable !== groupWritable) {
139
+ permissionUnverified("Windows descriptor ACL summary was inconsistent");
140
+ }
141
+ }
142
+ export function inspectSecureWindowsDescriptor(params) {
143
+ let native;
144
+ try {
145
+ native = getNativeBinding();
146
+ }
147
+ catch (cause) {
148
+ permissionUnverified("Windows descriptor ACL verification requires the matching native helper", cause);
149
+ }
150
+ const inspect = native?.inspectWindowsSecureFileHandle;
151
+ if (typeof inspect !== "function") {
152
+ permissionUnverified("Windows descriptor ACL verification requires an up-to-date native helper");
153
+ }
154
+ let result;
155
+ try {
156
+ result = inspect.call(native, params.fd);
157
+ }
158
+ catch (cause) {
159
+ permissionUnverified("Windows descriptor ACL verification failed", cause);
160
+ }
161
+ if (!isRecord(result))
162
+ permissionUnverified("Windows descriptor ACL facts were malformed");
163
+ const observed = parseIdentity(result.identity);
164
+ if (observed.dev !== params.identity.dev || observed.ino !== params.identity.ino) {
165
+ identityMismatch();
166
+ }
167
+ const facts = validateSecurity(result.security);
168
+ validatePermissionSummary(facts);
169
+ return {
170
+ ok: true,
171
+ isSymlink: false,
172
+ isDir: params.stat.isDirectory(),
173
+ mode: typeof params.stat.mode === "number" ? params.stat.mode : null,
174
+ bits: null,
175
+ source: "windows-acl",
176
+ worldWritable: facts.worldWritable,
177
+ groupWritable: facts.groupWritable,
178
+ worldReadable: facts.worldReadable,
179
+ groupReadable: facts.groupReadable,
180
+ ownerSid: facts.ownerSid,
181
+ ownerTrusted: TRUSTED_OWNER_CLASSES.has(facts.ownerClass),
182
+ aclSummary: `native descriptor owner=${facts.ownerClass} world=` +
183
+ `${facts.worldReadable ? "r" : "-"}${facts.worldWritable ? "w" : "-"} ` +
184
+ `group=${facts.groupReadable ? "r" : "-"}${facts.groupWritable ? "w" : "-"}`,
185
+ };
186
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAc/D,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAI1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAmNF,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAuB/B"}
1
+ {"version":3,"file":"secure-file.d.ts","sourceRoot":"","sources":["../src/secure-file.ts"],"names":[],"mappings":"AAAA,OAAe,EAAoB,KAAK,KAAK,EAAE,MAAM,SAAS,CAAC;AAc/D,OAAO,EAOL,KAAK,eAAe,EACpB,KAAK,sBAAsB,EAC5B,MAAM,kBAAkB,CAAC;AAK1B,MAAM,MAAM,qBAAqB,GAAG;IAClC,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,sBAAsB,CAAC;IAC/B,WAAW,CAAC,EAAE,2BAA2B,CAAC;IAC1C,MAAM,CAAC,EAAE,uBAAuB,CAAC;IACjC,EAAE,CAAC,EAAE,mBAAmB,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,qBAAqB,CAAC,EAAE,OAAO,CAAC;CACjC,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG,sBAAsB,CAAC;AAE7D,MAAM,MAAM,mBAAmB,GAAG;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,IAAI,EAAE,KAAK,CAAC;IACZ,WAAW,CAAC,EAAE,eAAe,CAAC;CAC/B,CAAC;AAuNF,wBAAsB,cAAc,CAClC,OAAO,EAAE,qBAAqB,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CA6B/B"}
@@ -12,6 +12,7 @@ import { resolveReadOpenFlags } from "./read-open-flags.js";
12
12
  import { realpathSync } from "./realpath.js";
13
13
  import { inspectPathPermissions, isGroupReadable, isGroupWritable, isWorldReadable, isWorldWritable, modeBits, } from "./permissions.js";
14
14
  import { inspectFileIdentity } from "./strict-file-identity.js";
15
+ import { inspectSecureWindowsDescriptor } from "./secure-file-windows.js";
15
16
  import { scheduleTimeout } from "./timing.js";
16
17
  function isAbsolutePathname(value) {
17
18
  return (path.isAbsolute(value) ||
@@ -128,13 +129,15 @@ function inspectOpenedPermissions(stat, platform) {
128
129
  groupReadable: isGroupReadable(bits),
129
130
  };
130
131
  }
131
- async function assertSecurePermissions(options, stat, realPath) {
132
+ async function assertSecurePermissions(options, stat, realPath, identity, fd) {
132
133
  if (options.permissions?.allowInsecure) {
133
134
  return undefined;
134
135
  }
135
136
  const platform = options.inject?.platform ?? process.platform;
136
137
  const permissions = platform === "win32"
137
- ? await inspectPathPermissions(realPath, options.inject)
138
+ ? process.platform === "win32"
139
+ ? inspectSecureWindowsDescriptor({ fd, identity, stat })
140
+ : await inspectPathPermissions(realPath, options.inject)
138
141
  : inspectOpenedPermissions(stat, platform);
139
142
  const reason = permissions.error ? `: ${formatPermissionErrorDetail(permissions.error)}` : "";
140
143
  const diagnostics = {
@@ -194,7 +197,7 @@ export async function readSecureFile(options) {
194
197
  const opened = await openSecureHandle(options, maxBytes);
195
198
  try {
196
199
  await assertTrustedDirs(options, opened.realPath);
197
- const permissions = await assertSecurePermissions(options, opened.pathStat, opened.realPath);
200
+ const permissions = await assertSecurePermissions(options, opened.pathStat, opened.realPath, opened.identity, opened.handle.fd);
198
201
  const buffer = await readHandleWithTimeout(opened.handle, options.io?.timeoutMs, maxBytes);
199
202
  const finalIdentity = await inspectFileIdentity(() => fsSync.fstatSync(opened.handle.fd, { bigint: true }), opened.identity);
200
203
  if (!finalIdentity.isFile()) {
@@ -1,3 +1,4 @@
1
+ export declare function assertCallbackTempPathDeviceSafe(tempPath: string): void;
1
2
  export declare function writeCallbackSibling<T>(params: {
2
3
  tempPath: string;
3
4
  write: (tempPath: string) => Promise<T>;
@@ -1 +1 @@
1
- {"version":3,"file":"sibling-staged-file.d.ts","sourceRoot":"","sources":["../src/sibling-staged-file.ts"],"names":[],"mappings":"AAuEA,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,CA+G3C"}
1
+ {"version":3,"file":"sibling-staged-file.d.ts","sourceRoot":"","sources":["../src/sibling-staged-file.ts"],"names":[],"mappings":"AAuCA,wBAAgB,gCAAgC,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAIvE;AAmCD,wBAAsB,oBAAoB,CAAC,CAAC,EAAE,MAAM,EAAE;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IACxC,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,kEAAkE;IAClE,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,OAAO,CAAC;IACtB,aAAa,EAAE,OAAO,CAAC;CACxB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,CAAC,CAAA;CAAE,CAAC,CAgH3C"}
@@ -4,6 +4,7 @@ import fs, {} from "node:fs/promises";
4
4
  import path from "node:path";
5
5
  import { assertAsyncDirectoryGuard, assertDirectoryIdentitySync, createAsyncDirectoryGuard, } from "./directory-guard.js";
6
6
  import { syncDirectoryBestEffort } from "./directory-durability.js";
7
+ import { isWindowsReservedDeviceName } from "./device-path.js";
7
8
  import { FsSafeError } from "./errors.js";
8
9
  import { resolveReadOpenFlags } from "./read-open-flags.js";
9
10
  import { root } from "./root.js";
@@ -29,6 +30,11 @@ async function inspectStage(inspect, expected) {
29
30
  return stat;
30
31
  }, expected);
31
32
  }
33
+ export function assertCallbackTempPathDeviceSafe(tempPath) {
34
+ if (isWindowsReservedDeviceName(tempPath)) {
35
+ throw new FsSafeError("invalid-path", "callback temp path uses a reserved Windows device name");
36
+ }
37
+ }
32
38
  // Own the workspace before the producer can leave partial output. The finished
33
39
  // file still enters the ordinary sibling admission and publication lifecycle.
34
40
  async function writeIsolatedProducer(params) {
@@ -59,6 +65,7 @@ async function writeIsolatedProducer(params) {
59
65
  // Keep one descriptor and one exact identity through mode, sync, rename and cleanup.
60
66
  // Read/write access is needed only when the caller requests file synchronization.
61
67
  export async function writeCallbackSibling(params) {
68
+ assertCallbackTempPathDeviceSafe(params.tempPath);
62
69
  const parent = path.dirname(params.tempPath);
63
70
  const guard = await createAsyncDirectoryGuard(parent, { bigint: true });
64
71
  const assertParent = () => assertAsyncDirectoryGuard(guard);
@@ -1 +1 @@
1
- {"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uGAAuG;IACvG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yEAAyE;IACzE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AASF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAqBxC;AAwBD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6ChB"}
1
+ {"version":3,"file":"sibling-temp.d.ts","sourceRoot":"","sources":["../src/sibling-temp.ts"],"names":[],"mappings":"AAeA,MAAM,MAAM,2BAA2B,CAAC,CAAC,IAAI;IAC3C,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5C,oEAAoE;IACpE,iBAAiB,CAAC,EAAE,mBAAmB,CAAC;IACxC,gBAAgB,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,MAAM,CAAC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uGAAuG;IACvG,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mEAAmE;IACnE,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,yEAAyE;IACzE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,0BAA0B,CAAC,CAAC,IAAI;IAC1C,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,CAAC,CAAC;CACX,CAAC;AASF,wBAAsB,oBAAoB,CAAC,CAAC,EAC1C,OAAO,EAAE,2BAA2B,CAAC,CAAC,CAAC,GACtC,OAAO,CAAC,0BAA0B,CAAC,CAAC,CAAC,CAAC,CAqBxC;AAwBD,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,GAAG,OAAO,CAAC,IAAI,CAAC,CA8ChB"}
@@ -9,7 +9,7 @@ import { recursiveMkdirPath } from "./recursive-mkdir-path.js";
9
9
  import { root } from "./root.js";
10
10
  import { assertSafePathPrefix } from "./safe-path-segment.js";
11
11
  import { resolveSecureTempRoot } from "./secure-temp-dir.js";
12
- import { writeCallbackSibling } from "./sibling-staged-file.js";
12
+ import { assertCallbackTempPathDeviceSafe, writeCallbackSibling } from "./sibling-staged-file.js";
13
13
  import { tempFile } from "./temp-target.js";
14
14
  import { getFsSafeTestHooks } from "./test-hooks.js";
15
15
  function buildTempPath(dir, tempPrefix) {
@@ -93,6 +93,7 @@ export async function writeViaSiblingTempPath(params) {
93
93
  fallbackFileName: params.fallbackFileName ?? "output.bin",
94
94
  tempPrefix: params.tempPrefix ?? ".fs-safe-output-",
95
95
  });
96
+ assertCallbackTempPathDeviceSafe(tempPath);
96
97
  await getFsSafeTestHooks()?.beforeSiblingTempWrite?.(tempPath);
97
98
  await params.writeTemp(tempPath);
98
99
  await assertAsyncDirectoryGuard(rootGuard);
@@ -58,7 +58,7 @@ export async function acquireSidecarLock(options, context) {
58
58
  options.reentrantOwner !== undefined &&
59
59
  held.reentrantOwner !== undefined &&
60
60
  options.reentrantOwner === held.reentrantOwner) {
61
- held.refCount += 1;
61
+ held.refCount = (held.refCount ?? 1) + 1;
62
62
  // Retention is monotonic: any same-owner request to keep the sidecar on
63
63
  // exit upgrades the held lock; a later default acquisition never revokes it.
64
64
  if (options.retainOnExit === true) {
@@ -1 +1 @@
1
- {"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAsIjC,0FAA0F;AAC1F,wBAAgB,uBAAuB,IAAI,OAAO,CAMjD;AAuFD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM;cAU3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACpD,yBAAyB,CAAC,QAAQ,CAAC,KAC3C,OAAO,CAAC,iBAAiB,CAAC;eAkBL,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACxD,yBAAyB,CAAC,QAAQ,CAAC,MACxC,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAC;iBAqBW,OAAO,CAAC,IAAI,CAAC;iBAQnB,IAAI;uBAIE,oBAAoB,EAAE;EAW/C;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAMZ"}
1
+ {"version":3,"file":"sidecar-lock.d.ts","sourceRoot":"","sources":["../src/sidecar-lock.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,yBAAyB,EACzB,iBAAiB,EACjB,oBAAoB,EACpB,sBAAsB,EACvB,MAAM,yBAAyB,CAAC;AACjC,YAAY,EAAE,wBAAwB,EAAE,MAAM,2BAA2B,CAAC;AAC1E,YAAY,EACV,yBAAyB,EACzB,0BAA0B,EAC1B,iBAAiB,EACjB,oBAAoB,EACpB,uBAAuB,EACvB,wBAAwB,EACxB,sBAAsB,GACvB,MAAM,yBAAyB,CAAC;AAmIjC,0FAA0F;AAC1F,wBAAgB,uBAAuB,IAAI,OAAO,CAMjD;AAyFD,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM;cAU3B,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACpD,yBAAyB,CAAC,QAAQ,CAAC,KAC3C,OAAO,CAAC,iBAAiB,CAAC;eAkBL,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,WACxD,yBAAyB,CAAC,QAAQ,CAAC,MACxC,MAAM,OAAO,CAAC,CAAC,CAAC,KACnB,OAAO,CAAC,CAAC,CAAC;iBAqBW,OAAO,CAAC,IAAI,CAAC;iBAQnB,IAAI;uBAIE,oBAAoB,EAAE;EAW/C;AAED,wBAAsB,eAAe,CAAC,CAAC,EAAE,QAAQ,SAAS,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/E,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,sBAAsB,CAAC,QAAQ,CAAC,EACzC,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GACnB,OAAO,CAAC,CAAC,CAAC,CAMZ"}
@@ -37,9 +37,6 @@ function resolveManagerState(key) {
37
37
  // Backfill state created by fs-safe versions that predate reclaim guards.
38
38
  state.reclaimCleanupRegistered ??= false;
39
39
  state.reclaimGuards ??= new Set();
40
- for (const held of state.held.values()) {
41
- held.refCount ??= 1;
42
- }
43
40
  }
44
41
  return state;
45
42
  }
@@ -163,6 +160,8 @@ async function releaseHeldLock(state, normalizedTargetPath, held, options = {})
163
160
  await held.releasePromise;
164
161
  return true;
165
162
  }
163
+ // Older package copies can add holders after this manager was constructed.
164
+ held.refCount ??= 1;
166
165
  if (options.force) {
167
166
  held.refCount = 0;
168
167
  }
@@ -1 +1 @@
1
- {"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAQ7B,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,CAAC;AA8DF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAI7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAyCD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC;IAC1E,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;CAC7D,CAAC,CA2BD;AAED,wBAAsB,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEzE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE,eAAe,EACvB,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
1
+ {"version":3,"file":"temp-target.d.ts","sourceRoot":"","sources":["../src/temp-target.ts"],"names":[],"mappings":"AACA,OAAO,MAAM,MAAM,SAAS,CAAC;AAS7B,MAAM,MAAM,QAAQ,GAAG;IACrB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAChC,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC,CAAC;AAEF,KAAK,eAAe,GAAG;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CAC3C,CAAC;AA8DF,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAK7D;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE;IAC9C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,GAAG,MAAM,CAaT;AAyCD,wBAAsB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC;IAC1E,MAAM,EAAE,QAAQ,CAAC;IACjB,QAAQ,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC;CAC7D,CAAC,CA2BD;AAED,wBAAsB,QAAQ,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,QAAQ,CAAC,CAEzE;AAED,wBAAsB,YAAY,CAAC,CAAC,EAClC,MAAM,EAAE,eAAe,EACvB,EAAE,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,GAClC,OAAO,CAAC,CAAC,CAAC,CAOZ"}
@@ -2,6 +2,7 @@ import crypto from "node:crypto";
2
2
  import fsSync from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import path from "node:path";
5
+ import { suffixWindowsReservedDeviceName } from "./filename.js";
5
6
  import { sameFileIdentityForCleanup } from "./file-identity.js";
6
7
  import { assertSafePathSegment, sanitizeSafePathSegment, trimHyphenEdges } from "./safe-path-segment.js";
7
8
  import { resolveSecureTempRoot } from "./secure-temp-dir.js";
@@ -57,9 +58,10 @@ function sanitizeExtension(extension) {
57
58
  return token ? `.${token}` : "";
58
59
  }
59
60
  export function sanitizeTempFileName(fileName) {
60
- return sanitizeSafePathSegment(path.basename(fileName), "download.bin", {
61
+ const sanitized = sanitizeSafePathSegment(path.basename(fileName), "download.bin", {
61
62
  allowDotPrefix: true,
62
63
  });
64
+ return suffixWindowsReservedDeviceName(sanitized);
63
65
  }
64
66
  export function buildRandomTempFilePath(params) {
65
67
  const rootDir = resolveTempRoot(params.rootDir);
@@ -1 +1 @@
1
- {"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAK7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AAgFF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CAyDjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CAyD1C"}
1
+ {"version":3,"file":"walk.d.ts","sourceRoot":"","sources":["../src/walk.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,SAAS,CAAC;AAK7B,MAAM,MAAM,aAAa,GAAG,MAAM,GAAG,WAAW,GAAG,SAAS,GAAG,OAAO,CAAC;AACvE,MAAM,MAAM,iBAAiB,GAAG,MAAM,GAAG,QAAQ,GAAG,SAAS,CAAC;AAE9D,MAAM,MAAM,kBAAkB,GAAG;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,aAAa,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC;CACvB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;IACjD,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,kBAAkB,KAAK,OAAO,CAAC;CAClD,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,kBAAkB,EAAE,CAAC;IAC9B,iBAAiB,EAAE,MAAM,CAAC;IAC1B,SAAS,EAAE,OAAO,CAAC;IAGnB,UAAU,CAAC,EAAE,oBAAoB,EAAE,CAAC;CACrC,CAAC;AAEF,KAAK,+BAA+B,GAAG,mBAAmB,GAAG;IAC3D,UAAU,EAAE,oBAAoB,EAAE,CAAC;CACpC,CAAC;AA+EF,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,+BAA+B,CA0DjC;AAED,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,+BAA+B,CAAC,CA0D1C"}
package/dist/walk.js CHANGED
@@ -29,11 +29,10 @@ function shouldStop(result, options) {
29
29
  }
30
30
  function buildEntry(params) {
31
31
  const fullPath = params.fullPath;
32
- const relativePath = path.relative(params.rootDir, fullPath) || params.dirent.name;
33
32
  return {
34
33
  name: params.dirent.name,
35
34
  path: fullPath,
36
- relativePath,
35
+ relativePath: params.relativePath,
37
36
  depth: params.depth,
38
37
  kind: params.kind ?? kindForDirent(params.dirent),
39
38
  dirent: params.dirent,
@@ -79,7 +78,7 @@ export function walkDirectorySync(rootDir, options = {}) {
79
78
  failedDirs: [],
80
79
  };
81
80
  const visitedDirs = new Set();
82
- function visit(dir, depth) {
81
+ function visit(dir, relativeDir, depth) {
83
82
  if (options.maxDepth !== undefined && depth > options.maxDepth)
84
83
  return;
85
84
  let realDir;
@@ -111,20 +110,21 @@ export function walkDirectorySync(rootDir, options = {}) {
111
110
  const kind = resolveKind(fullPath, dirent, symlinks);
112
111
  if (!kind)
113
112
  continue;
114
- const entry = buildEntry({ rootDir: root, fullPath, dirent, depth, kind });
113
+ const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
114
+ const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
115
115
  if (options.include?.(entry) ?? true) {
116
116
  result.entries.push(entry);
117
117
  }
118
118
  if (kind === "directory" &&
119
119
  (options.maxDepth === undefined || depth < options.maxDepth) &&
120
120
  (options.descend?.(entry) ?? true)) {
121
- visit(fullPath, depth + 1);
121
+ visit(fullPath, relativePath, depth + 1);
122
122
  if (result.truncated)
123
123
  return;
124
124
  }
125
125
  }
126
126
  }
127
- visit(root, 1);
127
+ visit(root, "", 1);
128
128
  return result;
129
129
  }
130
130
  export async function walkDirectory(rootDir, options = {}) {
@@ -138,7 +138,7 @@ export async function walkDirectory(rootDir, options = {}) {
138
138
  failedDirs: [],
139
139
  };
140
140
  const visitedDirs = new Set();
141
- async function visit(dir, depth) {
141
+ async function visit(dir, relativeDir, depth) {
142
142
  if (options.maxDepth !== undefined && depth > options.maxDepth)
143
143
  return;
144
144
  let realDir;
@@ -170,19 +170,20 @@ export async function walkDirectory(rootDir, options = {}) {
170
170
  const kind = resolveKind(fullPath, dirent, symlinks);
171
171
  if (!kind)
172
172
  continue;
173
- const entry = buildEntry({ rootDir: root, fullPath, dirent, depth, kind });
173
+ const relativePath = relativeDir ? `${relativeDir}${path.sep}${dirent.name}` : dirent.name;
174
+ const entry = buildEntry({ relativePath, fullPath, dirent, depth, kind });
174
175
  if (options.include?.(entry) ?? true) {
175
176
  result.entries.push(entry);
176
177
  }
177
178
  if (kind === "directory" &&
178
179
  (options.maxDepth === undefined || depth < options.maxDepth) &&
179
180
  (options.descend?.(entry) ?? true)) {
180
- await visit(fullPath, depth + 1);
181
+ await visit(fullPath, relativePath, depth + 1);
181
182
  if (result.truncated)
182
183
  return;
183
184
  }
184
185
  }
185
186
  }
186
- await visit(root, 1);
187
+ await visit(root, "", 1);
187
188
  return result;
188
189
  }
package/docs/advanced.md CHANGED
@@ -96,6 +96,8 @@ byte limit. Continuation buffers grow only after filling with actual bytes,
96
96
  up to the byte budget plus its probe; file-size hints cannot force that growth.
97
97
  Unknown-size inputs start with at most 64 KiB. This avoids per-chunk copies and
98
98
  a final concatenation when a file exceeds the initial allocation.
99
+ Regular files that report a size of zero, such as virtual files, continue through
100
+ positive short reads until actual EOF or byte-limit overflow.
99
101
 
100
102
  The bounded descriptor helpers start at the descriptor's current offset and
101
103
  leave ownership with the caller. They are intended for the second half of a
@@ -133,7 +135,7 @@ component is followed by another segment, both helpers throw
133
135
 
134
136
  | Export | Page | Notes |
135
137
  |---|---|---|
136
- | `safeDirName`, `safePathSegmentHashed`, `resolveSafeInstallDir`, `assertCanonicalPathWithinBase` | [install-path.md](install-path.md) | Build install-target directories from caller-supplied identifiers. |
138
+ | `safeDirName`, `safePathSegmentHashed`, `safePathSegmentHashedV2`, `resolveSafeInstallDir`, `assertCanonicalPathWithinBase` | [install-path.md](install-path.md) | Build install-target directories; use V2 for untrusted identifier mappings. |
137
139
  | `sanitizeUntrustedFileName` | [filename.md](filename.md) | Coerce an untrusted string into a safe filename. |
138
140
  | `resolveHomeRelativePath` | – | Expand a leading `~` before resolving `.` and `..`; tildes inside relative paths stay literal. |
139
141
 
package/docs/archive.md CHANGED
@@ -141,6 +141,12 @@ between native and JavaScript paths rather than reimplementing it in Rust.
141
141
 
142
142
  ZIP extraction and bounded reads admit every physical central-directory record and its referenced local header before either decoder can normalize or collapse names. Raw names and valid Unicode Path names must pass traversal checks before stripping, filtering, or selecting a requested member; duplicate or colliding names reject with `entry-path`, even in unrelated or skipped members. Materially conflicting local/central or Unicode interpretations, malformed critical metadata, and ambiguous framing reject with `ArchiveFormatError`. Harmless separator and dot-component equivalence is allowed only after validation. Ordinary legacy filename decoding remains backend-selected. Native ZIP extraction groups nearby metadata reads into at most two 4 KiB read-ahead buffers per admission pass; larger records retain separately bounded reads. Buffered record views remain stable across eviction, and cached work periodically yields for deadline checks.
143
143
 
144
+ Within one ZIP entry, identical local and central name bytes reuse the same
145
+ decoded validation. Unicode Path admission is shared only when both the raw names
146
+ and the complete Unicode fields match; different fields still verify their own
147
+ CRC and interpretation. Shared backing memory is checked independently. Decoded
148
+ name validation is not reused across entries or archives.
149
+
144
150
  `stripComponents` removes leading nonempty, non-`.` path components after
145
151
  normalizing separators. For example, `./pkg/hello.txt` with
146
152
  `stripComponents: 1` extracts to `hello.txt` on both backends. Entries with no