@openclaw/fs-safe 0.6.0 → 0.7.1

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 (202) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/README.md +7 -0
  3. package/dist/archive-deadline.d.ts +3 -0
  4. package/dist/archive-deadline.d.ts.map +1 -1
  5. package/dist/archive-deadline.js +44 -8
  6. package/dist/archive-entry.d.ts.map +1 -1
  7. package/dist/archive-entry.js +1 -0
  8. package/dist/archive-errors.d.ts +1 -0
  9. package/dist/archive-errors.d.ts.map +1 -1
  10. package/dist/archive-errors.js +3 -0
  11. package/dist/archive-input.d.ts.map +1 -1
  12. package/dist/archive-input.js +26 -16
  13. package/dist/archive-limits.d.ts +11 -3
  14. package/dist/archive-limits.d.ts.map +1 -1
  15. package/dist/archive-limits.js +24 -0
  16. package/dist/archive-native.d.ts +3 -2
  17. package/dist/archive-native.d.ts.map +1 -1
  18. package/dist/archive-native.js +24 -9
  19. package/dist/archive-policy.d.ts +2 -0
  20. package/dist/archive-policy.d.ts.map +1 -1
  21. package/dist/archive-policy.js +9 -1
  22. package/dist/archive-read.d.ts.map +1 -1
  23. package/dist/archive-read.js +69 -41
  24. package/dist/archive-staging.d.ts +3 -0
  25. package/dist/archive-staging.d.ts.map +1 -1
  26. package/dist/archive-staging.js +91 -43
  27. package/dist/archive-tar-admission.d.ts +7 -0
  28. package/dist/archive-tar-admission.d.ts.map +1 -0
  29. package/dist/archive-tar-admission.js +43 -0
  30. package/dist/archive-tar-gnu.d.ts +2 -0
  31. package/dist/archive-tar-gnu.d.ts.map +1 -0
  32. package/dist/archive-tar-gnu.js +20 -0
  33. package/dist/archive-tar-header.d.ts +8 -0
  34. package/dist/archive-tar-header.d.ts.map +1 -0
  35. package/dist/archive-tar-header.js +47 -0
  36. package/dist/archive-tar-meta.d.ts +29 -1
  37. package/dist/archive-tar-meta.d.ts.map +1 -1
  38. package/dist/archive-tar-meta.js +104 -35
  39. package/dist/archive-tar-runtime.d.ts +3 -0
  40. package/dist/archive-tar-runtime.d.ts.map +1 -1
  41. package/dist/archive-tar-runtime.js +3 -0
  42. package/dist/archive-tar.d.ts.map +1 -1
  43. package/dist/archive-tar.js +8 -2
  44. package/dist/archive-zip-names.js +2 -2
  45. package/dist/archive.d.ts.map +1 -1
  46. package/dist/archive.js +38 -27
  47. package/dist/atomic.d.ts +1 -1
  48. package/dist/atomic.d.ts.map +1 -1
  49. package/dist/bounded-read-stream.d.ts.map +1 -1
  50. package/dist/bounded-read-stream.js +2 -4
  51. package/dist/bounded-read.d.ts.map +1 -1
  52. package/dist/bounded-read.js +3 -10
  53. package/dist/byte-budget.d.ts +5 -0
  54. package/dist/byte-budget.d.ts.map +1 -0
  55. package/dist/byte-budget.js +9 -0
  56. package/dist/directory-guard.d.ts +2 -1
  57. package/dist/directory-guard.d.ts.map +1 -1
  58. package/dist/directory-guard.js +10 -0
  59. package/dist/file-lock-sync.d.ts.map +1 -1
  60. package/dist/file-lock-sync.js +81 -30
  61. package/dist/file-observation.d.ts +9 -0
  62. package/dist/file-observation.d.ts.map +1 -0
  63. package/dist/file-observation.js +22 -0
  64. package/dist/file-store-boundary.d.ts.map +1 -1
  65. package/dist/file-store-boundary.js +9 -5
  66. package/dist/file-store-limit.d.ts +2 -0
  67. package/dist/file-store-limit.d.ts.map +1 -0
  68. package/dist/file-store-limit.js +8 -0
  69. package/dist/file-store-sync-write.d.ts.map +1 -1
  70. package/dist/file-store-sync-write.js +38 -8
  71. package/dist/file-store.d.ts.map +1 -1
  72. package/dist/file-store.js +26 -26
  73. package/dist/json-durable-queue-directory.d.ts +2 -0
  74. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  75. package/dist/json-durable-queue-directory.js +20 -0
  76. package/dist/json-durable-queue-ownership.d.ts +14 -0
  77. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  78. package/dist/json-durable-queue-ownership.js +168 -0
  79. package/dist/json-durable-queue-retirement.d.ts +9 -0
  80. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  81. package/dist/json-durable-queue-retirement.js +126 -0
  82. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  83. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  84. package/dist/json-durable-queue-transfer-lock.js +19 -0
  85. package/dist/json-durable-queue.d.ts +1 -0
  86. package/dist/json-durable-queue.d.ts.map +1 -1
  87. package/dist/json-durable-queue.js +90 -57
  88. package/dist/json.d.ts.map +1 -1
  89. package/dist/json.js +32 -13
  90. package/dist/local-roots.d.ts.map +1 -1
  91. package/dist/local-roots.js +4 -2
  92. package/dist/native-binding.d.ts +12 -3
  93. package/dist/native-binding.d.ts.map +1 -1
  94. package/dist/native-pinned-write-windows.js +1 -1
  95. package/dist/opened-file-failure.d.ts +7 -0
  96. package/dist/opened-file-failure.d.ts.map +1 -0
  97. package/dist/opened-file-failure.js +41 -0
  98. package/dist/opened-realpath.d.ts.map +1 -1
  99. package/dist/opened-realpath.js +8 -2
  100. package/dist/output-sibling.d.ts.map +1 -1
  101. package/dist/output-sibling.js +11 -110
  102. package/dist/output.d.ts.map +1 -1
  103. package/dist/output.js +4 -2
  104. package/dist/pinned-open.d.ts.map +1 -1
  105. package/dist/pinned-open.js +27 -48
  106. package/dist/pinned-write.d.ts.map +1 -1
  107. package/dist/pinned-write.js +10 -10
  108. package/dist/private-temp-workspace.d.ts +3 -1
  109. package/dist/private-temp-workspace.d.ts.map +1 -1
  110. package/dist/private-temp-workspace.js +81 -56
  111. package/dist/publish-file.d.ts.map +1 -1
  112. package/dist/publish-file.js +2 -4
  113. package/dist/read-opened-file.d.ts.map +1 -1
  114. package/dist/read-opened-file.js +6 -4
  115. package/dist/regular-file.d.ts.map +1 -1
  116. package/dist/regular-file.js +136 -82
  117. package/dist/replace-file-copy-fallback.d.ts +3 -1
  118. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  119. package/dist/replace-file-copy-fallback.js +21 -36
  120. package/dist/replace-file-copy-source.d.ts +21 -0
  121. package/dist/replace-file-copy-source.d.ts.map +1 -0
  122. package/dist/replace-file-copy-source.js +112 -0
  123. package/dist/replace-file-descriptor.d.ts +13 -3
  124. package/dist/replace-file-descriptor.d.ts.map +1 -1
  125. package/dist/replace-file-descriptor.js +32 -7
  126. package/dist/replace-file-rename-policy.d.ts +7 -0
  127. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  128. package/dist/replace-file-rename-policy.js +30 -0
  129. package/dist/replace-file-temp-owner.d.ts +46 -0
  130. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  131. package/dist/replace-file-temp-owner.js +346 -0
  132. package/dist/replace-file.d.ts +6 -1
  133. package/dist/replace-file.d.ts.map +1 -1
  134. package/dist/replace-file.js +72 -58
  135. package/dist/root-impl.d.ts.map +1 -1
  136. package/dist/root-impl.js +113 -82
  137. package/dist/secret-file.d.ts.map +1 -1
  138. package/dist/secret-file.js +6 -3
  139. package/dist/secret-read-async.d.ts.map +1 -1
  140. package/dist/secret-read-async.js +6 -5
  141. package/dist/secure-file.d.ts.map +1 -1
  142. package/dist/secure-file.js +17 -13
  143. package/dist/sibling-staged-file.d.ts +15 -0
  144. package/dist/sibling-staged-file.d.ts.map +1 -0
  145. package/dist/sibling-staged-file.js +148 -0
  146. package/dist/sibling-temp.d.ts +3 -0
  147. package/dist/sibling-temp.d.ts.map +1 -1
  148. package/dist/sibling-temp.js +30 -74
  149. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  150. package/dist/sidecar-lock-acquire.js +91 -37
  151. package/dist/sidecar-lock-handle.d.ts +6 -2
  152. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  153. package/dist/sidecar-lock-handle.js +17 -3
  154. package/dist/sidecar-lock-policy.d.ts +2 -0
  155. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  156. package/dist/sidecar-lock-policy.js +29 -0
  157. package/dist/sidecar-lock-reclaim.d.ts +16 -5
  158. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  159. package/dist/sidecar-lock-reclaim.js +60 -28
  160. package/dist/sidecar-lock-root.d.ts +3 -0
  161. package/dist/sidecar-lock-root.d.ts.map +1 -0
  162. package/dist/sidecar-lock-root.js +76 -0
  163. package/dist/sidecar-lock.d.ts.map +1 -1
  164. package/dist/sidecar-lock.js +28 -16
  165. package/dist/strict-file-identity.d.ts.map +1 -1
  166. package/dist/strict-file-identity.js +4 -1
  167. package/dist/suppressed-error.d.ts +6 -0
  168. package/dist/suppressed-error.d.ts.map +1 -0
  169. package/dist/suppressed-error.js +15 -0
  170. package/dist/temp-cleanup.d.ts +2 -0
  171. package/dist/temp-cleanup.d.ts.map +1 -1
  172. package/dist/temp-cleanup.js +25 -10
  173. package/dist/temp-workspace-owner.d.ts +23 -0
  174. package/dist/temp-workspace-owner.d.ts.map +1 -0
  175. package/dist/temp-workspace-owner.js +320 -0
  176. package/dist/temp.d.ts +1 -1
  177. package/dist/temp.d.ts.map +1 -1
  178. package/dist/test-hooks.d.ts +5 -0
  179. package/dist/test-hooks.d.ts.map +1 -1
  180. package/docs/advanced.md +8 -1
  181. package/docs/archive.md +202 -26
  182. package/docs/atomic.md +11 -2
  183. package/docs/durability.md +1 -1
  184. package/docs/file-store.md +2 -0
  185. package/docs/install.md +8 -4
  186. package/docs/json.md +11 -4
  187. package/docs/native-helper.md +15 -3
  188. package/docs/native.md +33 -6
  189. package/docs/output.md +16 -3
  190. package/docs/public-api.md +6 -3
  191. package/docs/quickstart.md +1 -1
  192. package/docs/reading.md +1 -1
  193. package/docs/regular-file.md +9 -2
  194. package/docs/root.md +2 -0
  195. package/docs/secret-file.md +5 -1
  196. package/docs/secure-file.md +4 -2
  197. package/docs/security-model.md +18 -0
  198. package/docs/sidecar-lock.md +60 -4
  199. package/docs/store.md +13 -0
  200. package/docs/temp.md +128 -14
  201. package/docs/writing.md +6 -0
  202. package/package.json +10 -10
@@ -1,6 +1,34 @@
1
+ import { Transform } from "node:stream";
2
+ import { type TarMeterLimits } from "./archive-limits.js";
3
+ import type { TarEntryInfo } from "./archive-tar.js";
4
+ export declare class TarMetadataMeter extends Transform {
5
+ private readonly limits;
6
+ private readonly onMember?;
7
+ private readonly block;
8
+ private blockLength;
9
+ private state;
10
+ private pendingPax;
11
+ private readonly pendingGnu;
12
+ private pendingGnuPath;
13
+ private zeroBlocks;
14
+ private entries;
15
+ private remainingDecodedBytes;
16
+ private manifestBytes;
17
+ constructor(limits: TarMeterLimits, onMember?: ((entry: TarEntryInfo) => void) | undefined);
18
+ private countMember;
19
+ private invalid;
20
+ private paddedSize;
21
+ private parseSize;
22
+ private finishHeader;
23
+ private finishSparseHeader;
24
+ private meter;
25
+ _transform(chunk: Buffer, _encoding: BufferEncoding, callback: (error?: Error | null, data?: Buffer) => void): void;
26
+ _flush(callback: (error?: Error | null) => void): void;
27
+ }
1
28
  export declare function preflightTarMetadata(params: {
2
29
  archivePath: string;
3
- maxMetaEntryBytes: number;
30
+ limits: TarMeterLimits;
4
31
  signal?: AbortSignal;
32
+ onMember?: (entry: TarEntryInfo) => void;
5
33
  }): Promise<void>;
6
34
  //# sourceMappingURL=archive-tar-meta.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"archive-tar-meta.d.ts","sourceRoot":"","sources":["../src/archive-tar-meta.ts"],"names":[],"mappings":"AA8LA,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GAAG,OAAO,CAAC,IAAI,CAAC,CAShB"}
1
+ {"version":3,"file":"archive-tar-meta.d.ts","sourceRoot":"","sources":["../src/archive-tar-meta.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAY,MAAM,aAAa,CAAC;AAIlD,OAAO,EAAqE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAI7H,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AAWrD,qBAAa,gBAAiB,SAAQ,SAAS;IAYjC,OAAO,CAAC,QAAQ,CAAC,MAAM;IAAkB,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAC;IAX/E,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAqB;IAC3C,OAAO,CAAC,WAAW,CAAK;IACxB,OAAO,CAAC,KAAK,CAAkC;IAC/C,OAAO,CAAC,UAAU,CAAuB;IACzC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAwB;IACnD,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,UAAU,CAAK;IACvB,OAAO,CAAC,OAAO,CAAK;IACpB,OAAO,CAAC,qBAAqB,CAAS;IACtC,OAAO,CAAC,aAAa,CAAK;IAE1B,YAA6B,MAAM,EAAE,cAAc,EAAmB,QAAQ,CAAC,GAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,aAAA,EAM7G;IAED,OAAO,CAAC,WAAW;IAOnB,OAAO,CAAC,OAAO;IAIf,OAAO,CAAC,UAAU;IAMlB,OAAO,CAAC,SAAS;IA4BjB,OAAO,CAAC,YAAY;IA6EpB,OAAO,CAAC,kBAAkB;IAgB1B,OAAO,CAAC,KAAK;IAyCJ,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,IAAI,GAAG,IAAI,CAc3H;IAEQ,MAAM,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,EAAE,KAAK,GAAG,IAAI,KAAK,IAAI,GAAG,IAAI,CAM9D;CACF;AAaD,wBAAsB,oBAAoB,CAAC,MAAM,EAAE;IACjD,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,cAAc,CAAC;IACvB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;CAC1C,GAAG,OAAO,CAAC,IAAI,CAAC,CAShB"}
@@ -3,22 +3,49 @@ import { Transform, Writable } from "node:stream";
3
3
  import { pipeline } from "node:stream/promises";
4
4
  import { createGunzip } from "node:zlib";
5
5
  import { ArchiveFormatError } from "./archive-errors.js";
6
- import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError } from "./archive-limits.js";
6
+ import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, tarManifestEntryCost } from "./archive-limits.js";
7
7
  import { parseLocalPax, paxMemberSize } from "./archive-tar-pax.js";
8
- class TarMetadataMeter extends Transform {
9
- maxMetaEntryBytes;
8
+ import { validateGnuMetadata } from "./archive-tar-gnu.js";
9
+ import { rawTarMember } from "./archive-tar-admission.js";
10
+ import { validateTarHeader } from "./archive-tar-header.js";
11
+ // Keep raw framing aligned with native/src/tar_meter.rs, before either parser.
12
+ export class TarMetadataMeter extends Transform {
13
+ limits;
14
+ onMember;
10
15
  block = Buffer.alloc(512);
11
16
  blockLength = 0;
12
17
  state = { kind: "header" };
13
18
  pendingPax;
14
- pendingGnu = false;
15
- constructor(maxMetaEntryBytes) {
19
+ pendingGnu = new Set();
20
+ pendingGnuPath;
21
+ zeroBlocks = 0;
22
+ entries = 0;
23
+ remainingDecodedBytes;
24
+ manifestBytes = 0;
25
+ constructor(limits, onMember) {
16
26
  super();
17
- this.maxMetaEntryBytes = maxMetaEntryBytes;
27
+ this.limits = limits;
28
+ this.onMember = onMember;
29
+ if (!Number.isSafeInteger(limits.maxDecodedBytes) || limits.maxDecodedBytes < 0) {
30
+ throw new RangeError("maxDecodedBytes must be a non-negative safe integer");
31
+ }
32
+ this.remainingDecodedBytes = limits.maxDecodedBytes;
33
+ }
34
+ countMember() {
35
+ if (this.entries >= this.limits.maxEntries) {
36
+ throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ENTRY_COUNT_EXCEEDS_LIMIT);
37
+ }
38
+ this.entries += 1;
18
39
  }
19
40
  invalid(message) {
20
41
  return new ArchiveFormatError(`invalid TAR header: ${message}`);
21
42
  }
43
+ paddedSize(size) {
44
+ const padded = Math.ceil(size / 512) * 512;
45
+ if (!Number.isSafeInteger(padded))
46
+ throw this.invalid("entry padding exceeds the safe integer range");
47
+ return padded;
48
+ }
22
49
  parseSize() {
23
50
  const field = this.block.subarray(124, 136);
24
51
  if ((field[0] ?? 0) & 0x80) {
@@ -35,10 +62,13 @@ class TarMetadataMeter extends Transform {
35
62
  return Number(value);
36
63
  }
37
64
  const zero = field.indexOf(0);
65
+ if (zero >= 0 && field.subarray(zero).some((byte) => byte !== 0 && byte !== 0x20)) {
66
+ throw this.invalid("size has non-padding bytes after NUL");
67
+ }
38
68
  const bytes = field.subarray(0, zero < 0 ? field.length : zero);
39
69
  if (bytes.some((byte) => byte > 0x7f))
40
70
  throw this.invalid("size is not ASCII octal");
41
- const text = bytes.toString("ascii").trim();
71
+ const text = bytes.toString("ascii").replace(/^ +| +$/g, "");
42
72
  if (!text || !/^[0-7]+$/.test(text))
43
73
  throw this.invalid("size is not valid octal");
44
74
  const value = Number.parseInt(text, 8);
@@ -50,54 +80,56 @@ class TarMetadataMeter extends Transform {
50
80
  if (this.block.every((byte) => byte === 0)) {
51
81
  if (this.pendingPax)
52
82
  throw this.invalid("dangling PAX metadata");
53
- this.pendingGnu = false;
83
+ if (this.pendingGnu.size)
84
+ throw this.invalid("dangling GNU metadata");
54
85
  this.blockLength = 0;
55
- this.state = { kind: "header" };
86
+ this.zeroBlocks += 1;
87
+ this.state = { kind: this.zeroBlocks === 2 ? "eof" : "header" };
56
88
  return;
57
89
  }
90
+ if (this.zeroBlocks !== 0)
91
+ throw this.invalid("nonzero header after one TAR zero block");
92
+ validateTarHeader(this.block);
58
93
  const nameEnd = this.block.subarray(0, 100).indexOf(0);
59
94
  const name = this.block.subarray(0, nameEnd < 0 ? 100 : nameEnd);
60
95
  if (name.length === 0) {
61
96
  throw this.invalid("entry path is empty");
62
97
  }
63
- if (this.block[156] !== 0x35 && name.at(-1) === 0x2f) {
98
+ if (![0x35, 0x44].includes(this.block[156]) && name.at(-1) === 0x2f) {
64
99
  throw this.invalid("non-directory entry path ends with a separator");
65
100
  }
66
101
  let size = this.parseSize();
102
+ let padded = this.paddedSize(size);
67
103
  const type = this.block[156];
68
- if ([0x78, 0x67, 0x4c, 0x4b, 0x58, 0x4e].includes(type) && size > this.maxMetaEntryBytes) {
104
+ // Directory/link headers cannot carry bodies; PAX/GNU metadata can.
105
+ if ([0x31, 0x32, 0x35].includes(type) && size !== 0) {
106
+ throw this.invalid("directory or link has a nonzero body size");
107
+ }
108
+ if ([0x78, 0x67, 0x4c, 0x4b, 0x58, 0x4e].includes(type) && size > this.limits.maxMetaEntryBytes) {
69
109
  throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.META_ENTRY_SIZE_EXCEEDS_LIMIT);
70
110
  }
71
111
  if (type === 0x67 || type === 0x58 || type === 0x4e) {
72
112
  throw this.invalid("global/old PAX and old GNU metadata are not supported");
73
113
  }
74
- if (type === 0x78) {
75
- if (this.pendingPax || this.pendingGnu || size === 0)
76
- throw this.invalid("empty, repeated or mixed PAX metadata");
114
+ if (type === 0x78 || type === 0x4c || type === 0x4b) {
115
+ const metadataType = type === 0x78 ? "pax" : type === 0x4c ? "L" : "K";
116
+ if (this.pendingPax || size === 0 || (metadataType === "pax" ? this.pendingGnu.size : this.pendingGnu.has(metadataType))) {
117
+ throw this.invalid("empty, repeated or mixed PAX/GNU metadata");
118
+ }
77
119
  const magic = this.block.subarray(257, 265).toString("latin1");
78
120
  if (magic !== "ustar\0" + "00" && magic !== "ustar \0")
79
- throw this.invalid("unrecognized PAX header format");
80
- const padded = Math.ceil(size / 512) * 512;
81
- if (!Number.isSafeInteger(padded))
82
- throw this.invalid("entry padding exceeds the safe integer range");
83
- this.state = { kind: "pax", body: Buffer.alloc(size), used: 0, padding: padded - size };
121
+ throw this.invalid("unrecognized PAX/GNU header format");
122
+ if (metadataType !== "pax")
123
+ this.pendingGnu.add(metadataType);
124
+ this.state = { kind: "metadata", type: metadataType, body: Buffer.alloc(size), used: 0, padding: padded - size };
84
125
  this.blockLength = 0;
85
126
  return;
86
127
  }
87
128
  // Sparse headers retain their metadata-limit-before-format-error ordering.
88
129
  if (this.pendingPax && type !== 0x53) {
89
130
  size = paxMemberSize(this.pendingPax, type, size, this.block);
90
- this.pendingPax = undefined;
91
- }
92
- if (type === 0x4c || type === 0x4b) {
93
- this.pendingGnu = true;
94
131
  }
95
- else {
96
- this.pendingGnu = false;
97
- }
98
- const padded = Math.ceil(size / 512) * 512;
99
- if (!Number.isSafeInteger(padded))
100
- throw this.invalid("entry padding exceeds the safe integer range");
132
+ padded = this.paddedSize(size);
101
133
  if (type === 0x53) {
102
134
  if (this.block[482] !== 0 && this.block[482] !== 1) {
103
135
  throw this.invalid("GNU sparse extension flag is not 0 or 1");
@@ -108,13 +140,27 @@ class TarMetadataMeter extends Transform {
108
140
  this.state = { kind: "sparse", dataRemaining: padded, metaBytes: 0 };
109
141
  }
110
142
  else {
143
+ this.countMember();
144
+ if (this.pendingGnuPath && /[\\/]$/.test(this.pendingGnuPath) && ![0x35, 0x44].includes(type)) {
145
+ throw this.invalid("GNU effective non-directory path ends with a separator");
146
+ }
147
+ const entry = rawTarMember(this.block, size, this.pendingPax?.path ?? this.pendingGnuPath);
148
+ const cost = tarManifestEntryCost(entry.path);
149
+ if (cost > this.limits.maxManifestBytes - this.manifestBytes) {
150
+ throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.MANIFEST_SIZE_EXCEEDS_LIMIT);
151
+ }
152
+ this.manifestBytes += cost;
153
+ this.onMember?.(entry);
154
+ this.pendingPax = undefined;
155
+ this.pendingGnu.clear();
156
+ this.pendingGnuPath = undefined;
111
157
  this.state = padded === 0 ? { kind: "header" } : { kind: "data", remaining: padded };
112
158
  }
113
159
  this.blockLength = 0;
114
160
  }
115
161
  finishSparseHeader(state) {
116
162
  const metaBytes = state.metaBytes + 512;
117
- if (metaBytes > this.maxMetaEntryBytes) {
163
+ if (metaBytes > this.limits.maxMetaEntryBytes) {
118
164
  throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.META_ENTRY_SIZE_EXCEEDS_LIMIT);
119
165
  }
120
166
  if (this.block[504] !== 0 && this.block[504] !== 1) {
@@ -131,14 +177,25 @@ class TarMetadataMeter extends Transform {
131
177
  meter(chunk) {
132
178
  let offset = 0;
133
179
  while (offset < chunk.length) {
134
- if (this.state.kind === "pax") {
180
+ if (this.state.kind === "eof") {
181
+ if (chunk.subarray(offset).some((byte) => byte !== 0))
182
+ throw this.invalid("nonzero data after TAR EOF");
183
+ return;
184
+ }
185
+ if (this.state.kind === "metadata") {
135
186
  const state = this.state;
136
187
  const take = Math.min(state.body.length - state.used, chunk.length - offset);
137
188
  chunk.copy(state.body, state.used, offset, offset + take);
138
189
  state.used += take;
139
190
  offset += take;
140
191
  if (state.used === state.body.length) {
141
- this.pendingPax = parseLocalPax(state.body);
192
+ if (state.type === "pax")
193
+ this.pendingPax = parseLocalPax(state.body);
194
+ else {
195
+ const name = validateGnuMetadata(state.body, state.type);
196
+ if (state.type === "L")
197
+ this.pendingGnuPath = name;
198
+ }
142
199
  this.state = state.padding === 0 ? { kind: "header" } : { kind: "data", remaining: state.padding };
143
200
  }
144
201
  continue;
@@ -164,7 +221,15 @@ class TarMetadataMeter extends Transform {
164
221
  }
165
222
  _transform(chunk, _encoding, callback) {
166
223
  try {
167
- this.meter(chunk);
224
+ const admitted = Math.min(chunk.length, this.remainingDecodedBytes);
225
+ this.meter(chunk.subarray(0, admitted));
226
+ this.remainingDecodedBytes -= admitted;
227
+ if (admitted < chunk.length) {
228
+ // Inspect only the overflow probe, never scan an unbounded tail.
229
+ if (this.state.kind === "eof" && chunk[admitted] !== 0)
230
+ throw this.invalid("nonzero data after TAR EOF");
231
+ throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.DECODED_SIZE_EXCEEDS_LIMIT);
232
+ }
168
233
  callback(null, chunk);
169
234
  }
170
235
  catch (error) {
@@ -174,8 +239,12 @@ class TarMetadataMeter extends Transform {
174
239
  _flush(callback) {
175
240
  if (this.pendingPax)
176
241
  callback(this.invalid("dangling PAX metadata"));
177
- else if (this.state.kind === "header" && this.blockLength === 0)
242
+ else if (this.pendingGnu.size)
243
+ callback(this.invalid("dangling GNU metadata"));
244
+ else if (this.state.kind === "eof")
178
245
  callback();
246
+ else if (this.state.kind === "header" && this.blockLength === 0)
247
+ callback(this.invalid("missing two-block TAR EOF"));
179
248
  else
180
249
  callback(this.invalid(this.state.kind === "header" ? "truncated TAR header" : "truncated TAR entry"));
181
250
  }
@@ -193,7 +262,7 @@ async function isGzip(filePath) {
193
262
  }
194
263
  export async function preflightTarMetadata(params) {
195
264
  const input = fs.createReadStream(params.archivePath);
196
- const meter = new TarMetadataMeter(params.maxMetaEntryBytes);
265
+ const meter = new TarMetadataMeter(params.limits, params.onMember);
197
266
  const sink = new Writable({ write(_chunk, _encoding, callback) { callback(); } });
198
267
  if (await isGzip(params.archivePath)) {
199
268
  await pipeline(input, createGunzip(), meter, sink, { signal: params.signal });
@@ -12,6 +12,7 @@ export type TarParser = NodeJS.WritableStream & {
12
12
  on(event: "error", listener: (error: Error) => void): TarParser;
13
13
  on(event: "end", listener: () => void): TarParser;
14
14
  };
15
+ export declare const NODE_TAR_RATIO_DISABLED: number;
15
16
  export type TarModule = {
16
17
  Parser: new (options: {
17
18
  strict: true;
@@ -28,6 +29,7 @@ export type TarModule = {
28
29
  noMtime: true;
29
30
  strict: true;
30
31
  maxMetaEntrySize: number;
32
+ maxDecompressionRatio: number;
31
33
  filter?(this: TarParser, entryPath: string, entry: unknown): boolean;
32
34
  onReadEntry(this: unknown, entry: unknown): void;
33
35
  }): TarParser;
@@ -35,6 +37,7 @@ export type TarModule = {
35
37
  file: string;
36
38
  strict: true;
37
39
  maxMetaEntrySize: number;
40
+ maxDecompressionRatio: number;
38
41
  onReadEntry(entry: AsyncIterable<unknown> & {
39
42
  resume(): void;
40
43
  }): void;
@@ -1 +1 @@
1
- {"version":3,"file":"archive-tar-runtime.d.ts","sourceRoot":"","sources":["../src/archive-tar-runtime.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,GAAG;IAC9C,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC1B,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IAChF,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IACzE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;IACnE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,SAAS,CAAC;IAChE,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,SAAS,CAAC;CACnD,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,KAAK,OAAO,EAAE;QAAE,MAAM,EAAE,IAAI,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,CAAC;IAC/E,CAAC,CAAC,OAAO,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,aAAa,EAAE,KAAK,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC;QACd,aAAa,EAAE,KAAK,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC;QACd,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,MAAM,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;QACrE,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;KAClD,GAAG,SAAS,CAAC;IACd,CAAC,CAAC,OAAO,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG;YAAE,MAAM,IAAI,IAAI,CAAA;SAAE,GAAG,IAAI,CAAC;KACvE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,CAAC;AAEF,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,SAAS,CAAC,CAS5D;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS/D"}
1
+ {"version":3,"file":"archive-tar-runtime.d.ts","sourceRoot":"","sources":["../src/archive-tar-runtime.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,cAAc,GAAG;IAC3B,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,IAAI,IAAI,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,SAAS,GAAG,MAAM,CAAC,cAAc,GAAG;IAC9C,KAAK,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IAC1B,EAAE,CAAC,KAAK,EAAE,cAAc,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IAChF,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,GAAG,SAAS,CAAC;IACzE,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,GAAG,SAAS,CAAC;IACnE,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,GAAG,SAAS,CAAC;IAChE,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,MAAM,IAAI,GAAG,SAAS,CAAC;CACnD,CAAC;AAIF,eAAO,MAAM,uBAAuB,QAA2B,CAAC;AAEhE,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,KAAK,OAAO,EAAE;QAAE,MAAM,EAAE,IAAI,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE,KAAK,SAAS,CAAC;IAC/E,CAAC,CAAC,OAAO,EAAE;QACT,GAAG,EAAE,MAAM,CAAC;QACZ,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,MAAM,CAAC,EAAE,WAAW,CAAC;QACrB,aAAa,EAAE,KAAK,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC;QACd,aAAa,EAAE,KAAK,CAAC;QACrB,OAAO,EAAE,IAAI,CAAC;QACd,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,MAAM,CAAC,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,OAAO,CAAC;QACrE,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI,CAAC;KAClD,GAAG,SAAS,CAAC;IACd,CAAC,CAAC,OAAO,EAAE;QACT,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,IAAI,CAAC;QACb,gBAAgB,EAAE,MAAM,CAAC;QACzB,qBAAqB,EAAE,MAAM,CAAC;QAC9B,WAAW,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,GAAG;YAAE,MAAM,IAAI,IAAI,CAAA;SAAE,GAAG,IAAI,CAAC;KACvE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACtB,CAAC;AAEF,wBAAsB,iBAAiB,IAAI,OAAO,CAAC,SAAS,CAAC,CAS5D;AAED,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAS/D"}
@@ -1,4 +1,7 @@
1
1
  import { ArchiveFormatError } from "./archive-errors.js";
2
+ // Both callers parse an immutable staged stream only after fs-safe's complete
3
+ // decoded-byte admission, so node-tar must not add backend-specific policy.
4
+ export const NODE_TAR_RATIO_DISABLED = Number.POSITIVE_INFINITY;
2
5
  export async function importOptionalTar() {
3
6
  try {
4
7
  return await import("tar");
@@ -1 +1 @@
1
- {"version":3,"file":"archive-tar.d.ts","sourceRoot":"","sources":["../src/archive-tar.ts"],"names":[],"mappings":"AAMA,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAI7B,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAWvF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAyB7D;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAgDnC"}
1
+ {"version":3,"file":"archive-tar.d.ts","sourceRoot":"","sources":["../src/archive-tar.ts"],"names":[],"mappings":"AAMA,OAAO,EAKL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAGL,KAAK,kBAAkB,EACvB,KAAK,0BAA0B,EAChC,MAAM,qBAAqB,CAAC;AAI7B,MAAM,MAAM,YAAY,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAWvF,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,OAAO,GAAG,YAAY,CAyB7D;AAED,wBAAgB,8BAA8B,CAAC,MAAM,EAAE;IACrD,OAAO,EAAE,MAAM,CAAC;IAChB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,kBAAkB,CAAC;IACjC,UAAU,CAAC,EAAE,0BAA0B,CAAC;CACzC,GAAG,CAAC,KAAK,EAAE,YAAY,KAAK,OAAO,CAqDnC"}
@@ -43,7 +43,10 @@ export function createTarEntryPreflightChecker(params) {
43
43
  entryCount += 1;
44
44
  assertArchiveEntryCountWithinLimit(entryCount, limits);
45
45
  validateArchiveEntryPath(entry.path, { escapeLabel: params.escapeLabel });
46
- const relPath = stripArchivePath(entry.path, strip);
46
+ const canonicalPath = stripArchivePath(entry.path, 0);
47
+ if (!canonicalPath)
48
+ return false;
49
+ const relPath = stripArchivePath(canonicalPath, strip);
47
50
  if (!relPath) {
48
51
  return false;
49
52
  }
@@ -60,13 +63,16 @@ export function createTarEntryPreflightChecker(params) {
60
63
  if (!shouldExtractArchiveEntry({
61
64
  filter: params.entryFilter,
62
65
  onFiltered: params.onFiltered,
63
- entry: { path: entry.path, kind, size: entry.size },
66
+ entry: { path: canonicalPath, kind, size: entry.size },
64
67
  })) {
65
68
  return false;
66
69
  }
67
70
  if (BLOCKED_TAR_ENTRY_TYPES.has(entry.type)) {
68
71
  throw new ArchiveSecurityError("entry-link", `tar entry is a link: ${formatErrorDetail(entry.path)}`);
69
72
  }
73
+ // Accepted unsupported records remain omitted, as in the native plan.
74
+ if (kind === "other")
75
+ return false;
70
76
  budget.startEntry();
71
77
  budget.addEntrySize(entry.size);
72
78
  return true;
@@ -1,5 +1,5 @@
1
1
  import { ArchiveFormatError, ArchiveSecurityError } from "./archive-errors.js";
2
- import { validateArchiveEntryPath } from "./archive-entry.js";
2
+ import { stripArchivePath, validateArchiveEntryPath } from "./archive-entry.js";
3
3
  import { updateCrc32 } from "./archive-crc32.js";
4
4
  export function zipFormat(message) {
5
5
  throw new ArchiveFormatError(`invalid ZIP metadata: ${message}`);
@@ -40,7 +40,7 @@ function utf8(bytes) {
40
40
  }
41
41
  }
42
42
  function key(name) {
43
- return name.replaceAll("\\", "/").split("/").filter((part) => part && part !== ".").join("/");
43
+ return stripArchivePath(name, 0) ?? "";
44
44
  }
45
45
  function originalName(name, flags) {
46
46
  // ASCII path syntax is encoding-independent. Do not reinterpret legacy bytes
@@ -1 +1 @@
1
- {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AA4DA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACjF,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,6BAA6B,EAC7B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,iCAAiC,EACjC,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EACjC,4BAA4B,EAC5B,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,8BAA8B,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC;AAoNpC,wBAAsB,cAAc,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CAwKjF"}
1
+ {"version":3,"file":"archive.d.ts","sourceRoot":"","sources":["../src/archive.ts"],"names":[],"mappings":"AA+DA,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AAElE,YAAY,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC;AACjF,YAAY,EACV,kBAAkB,EAClB,gBAAgB,EAChB,sBAAsB,EACtB,0BAA0B,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EACL,kBAAkB,EAClB,yBAAyB,EACzB,wBAAwB,EACxB,gBAAgB,EAChB,wBAAwB,GACzB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,KAAK,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC/F,OAAO,EAAE,gBAAgB,EAAE,MAAM,mBAAmB,CAAC;AACrD,OAAO,EACL,wBAAwB,EACxB,iBAAiB,EACjB,6BAA6B,EAC7B,mBAAmB,EACnB,2BAA2B,EAC3B,uBAAuB,EACvB,4BAA4B,EAC5B,iCAAiC,EACjC,KAAK,oBAAoB,EACzB,KAAK,qBAAqB,GAC3B,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,kBAAkB,EAAE,KAAK,sBAAsB,EAAE,MAAM,qBAAqB,CAAC;AACtF,OAAO,EAAE,oBAAoB,EAAE,KAAK,wBAAwB,EAAE,MAAM,sBAAsB,CAAC;AAC3F,OAAO,EACL,kCAAkC,EAClC,iCAAiC,EACjC,4BAA4B,EAC5B,wBAAwB,EACxB,4BAA4B,GAC7B,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,8BAA8B,EAAE,KAAK,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrF,OAAO,EACL,2BAA2B,EAC3B,iCAAiC,EACjC,KAAK,mBAAmB,GACzB,MAAM,4BAA4B,CAAC;AAyNpC,wBAAsB,cAAc,CAAC,MAAM,EAAE,qBAAqB,GAAG,OAAO,CAAC,IAAI,CAAC,CA6KjF"}
package/dist/archive.js CHANGED
@@ -5,7 +5,7 @@ import { Readable } from "node:stream";
5
5
  import { pipeline } from "node:stream/promises";
6
6
  import { createArchiveOutputPathTracker, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
7
7
  import { createPipelineTimeoutError, waitForDeadline, withExtractionDeadline, } from "./archive-deadline.js";
8
- import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, createExtractBudgetTransform, resolveExtractLimits, } from "./archive-limits.js";
8
+ import { assertArchiveEntryCountWithinLimit, assertArchiveEntryPathComponentsWithinLimit, createByteBudgetTracker, createExtractBudgetTransform, resolveExtractLimits, resolveTarMeterLimits, } from "./archive-limits.js";
9
9
  import { resolveArchiveKind } from "./archive-kind.js";
10
10
  import { mergeExtractedTreeIntoDestination, prepareArchiveDestinationDir, prepareArchiveOutputPath, withStagedArchiveDestination, } from "./archive-staging.js";
11
11
  import { createTarEntryPreflightChecker, readTarEntryInfo, } from "./archive-tar.js";
@@ -17,9 +17,10 @@ import { ArchiveSecurityError } from "./archive-errors.js";
17
17
  import { extractNativeArchive } from "./archive-native.js";
18
18
  import { stageArchiveFileForExtraction } from "./archive-input.js";
19
19
  import { getNativeBinding } from "./native.js";
20
- import { resolveArchiveEntryMode, shouldExtractArchiveEntry, } from "./archive-policy.js";
21
- import { importOptionalTar, normalizeTarParserError } from "./archive-tar-runtime.js";
20
+ import { resolveArchiveEntryMode, resolveArchiveFilteredEntryPolicy, shouldExtractArchiveEntry, } from "./archive-policy.js";
21
+ import { importOptionalTar, NODE_TAR_RATIO_DISABLED, normalizeTarParserError } from "./archive-tar-runtime.js";
22
22
  import { preflightTarMetadata } from "./archive-tar-meta.js";
23
+ import { createTarAdmissionPlan } from "./archive-tar-admission.js";
23
24
  import { writeSiblingTempFile } from "./sibling-temp.js";
24
25
  export { isWindowsDrivePath, normalizeArchiveEntryPath, resolveArchiveOutputPath, stripArchivePath, validateArchiveEntryPath, } from "./archive-entry.js";
25
26
  export { resolveArchiveKind, resolvePackedRootDir } from "./archive-kind.js";
@@ -45,12 +46,16 @@ async function readZipEntryStream(entry) {
45
46
  }
46
47
  function resolveZipOutputPath(params) {
47
48
  validateArchiveEntryPath(params.entryPath);
48
- const relPath = stripArchivePath(params.entryPath, params.strip);
49
+ const canonicalPath = stripArchivePath(params.entryPath, 0);
50
+ if (!canonicalPath)
51
+ return null;
52
+ const relPath = stripArchivePath(canonicalPath, params.strip);
49
53
  if (!relPath) {
50
54
  return null;
51
55
  }
52
56
  validateArchiveEntryPath(relPath);
53
57
  return {
58
+ canonicalPath,
54
59
  relPath,
55
60
  outPath: resolveArchiveOutputPath({
56
61
  rootDir: params.destinationDir,
@@ -72,7 +77,6 @@ async function writeZipFileEntry(params) {
72
77
  try {
73
78
  await writeSiblingTempFile({
74
79
  dir: path.dirname(destinationPath),
75
- tempPrefix: `.${path.basename(destinationPath)}.fs-safe-archive`,
76
80
  chmodDir: false,
77
81
  mode: params.mode,
78
82
  writeTemp: async (tempPath) => {
@@ -151,7 +155,7 @@ async function extractZip(params) {
151
155
  if (!shouldExtractArchiveEntry({
152
156
  filter: params.entryFilter,
153
157
  onFiltered: params.onFiltered,
154
- entry: { path: entry.name, kind: entryKind, size: entrySize },
158
+ entry: { path: output.canonicalPath, kind: entryKind, size: entrySize },
155
159
  })) {
156
160
  continue;
157
161
  }
@@ -166,6 +170,7 @@ async function extractZip(params) {
166
170
  outPath: output.outPath,
167
171
  originalPath: entry.name,
168
172
  isDirectory: entry.dir,
173
+ deadline: params.deadline,
169
174
  });
170
175
  if (entry.dir) {
171
176
  await fs.chmod(output.outPath, mode);
@@ -184,6 +189,7 @@ async function extractZip(params) {
184
189
  sourceDir: stagingRealDir,
185
190
  destinationDir: params.destDir,
186
191
  destinationRealDir,
192
+ deadline: params.deadline,
187
193
  });
188
194
  params.deadline.check();
189
195
  },
@@ -194,11 +200,14 @@ async function extractZip(params) {
194
200
  }
195
201
  }
196
202
  export async function extractArchive(params) {
203
+ const onFiltered = resolveArchiveFilteredEntryPolicy(params.onFiltered);
197
204
  const kind = params.kind ?? resolveArchiveKind(params.archivePath);
198
205
  if (!kind) {
199
206
  throw new Error(`unsupported archive: ${params.archivePath}`);
200
207
  }
201
208
  const label = kind === "zip" ? "extract zip" : "extract tar";
209
+ const limits = resolveExtractLimits(params.limits);
210
+ const tarLimits = resolveTarMeterLimits(limits);
202
211
  const native = getNativeBinding();
203
212
  if (native) {
204
213
  await withExtractionDeadline(params.timeoutMs, label, async (deadline) => extractNativeArchive({
@@ -207,11 +216,12 @@ export async function extractArchive(params) {
207
216
  destDir: params.destDir,
208
217
  kind,
209
218
  stripComponents: params.stripComponents,
210
- limits: params.limits,
219
+ limits,
220
+ tarLimits,
211
221
  deadline,
212
222
  entryModes: params.entryModes,
213
223
  entryFilter: params.entryFilter,
214
- onFiltered: params.onFiltered,
224
+ onFiltered,
215
225
  }));
216
226
  return;
217
227
  }
@@ -222,17 +232,18 @@ export async function extractArchive(params) {
222
232
  if (kind === "tar") {
223
233
  await withExtractionDeadline(params.timeoutMs, label, async (deadline) => {
224
234
  const tar = await importOptionalTar();
225
- const limits = resolveExtractLimits(params.limits);
226
235
  const stagedArchive = await stageArchiveFileForExtraction({
227
236
  archivePath: params.archivePath,
228
237
  limits,
229
238
  deadline,
230
239
  });
231
240
  try {
241
+ const manifest = [];
232
242
  await preflightTarMetadata({
233
243
  archivePath: stagedArchive.path,
234
- maxMetaEntryBytes: limits.maxMetaEntryBytes,
244
+ limits: tarLimits,
235
245
  signal: deadline.signal,
246
+ onMember: (entry) => { manifest.push(entry); },
236
247
  });
237
248
  deadline.check();
238
249
  const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
@@ -246,13 +257,14 @@ export async function extractArchive(params) {
246
257
  stripComponents: params.stripComponents,
247
258
  limits,
248
259
  entryFilter: params.entryFilter,
249
- onFiltered: params.onFiltered,
260
+ onFiltered,
250
261
  });
262
+ const admission = createTarAdmissionPlan(manifest, (entry) => {
263
+ deadline.check();
264
+ return checkTarEntrySafety(entry);
265
+ }, strip);
251
266
  const acceptedEntries = [];
252
- // A canonical cwd is not enough here: tar can still follow
253
- // pre-existing child symlinks in the live destination tree.
254
- // Extract into a private staging dir first, then merge through
255
- // the same safe-open boundary checks used by direct file writes.
267
+ // Extract privately, then merge through the safe-open boundary.
256
268
  const extractor = tar.x({
257
269
  cwd: stagingDir,
258
270
  // fs-safe owns stripping: node-tar counts the `.` and empty path
@@ -265,20 +277,16 @@ export async function extractArchive(params) {
265
277
  preserveOwner: false,
266
278
  noMtime: true,
267
279
  strict: true,
268
- maxMetaEntrySize: limits.maxMetaEntryBytes,
280
+ maxMetaEntrySize: tarLimits.maxMetaEntryBytes,
281
+ maxDecompressionRatio: NODE_TAR_RATIO_DISABLED,
269
282
  filter(_entryPath, entry) {
270
283
  try {
271
284
  const info = readTarEntryInfo(entry);
272
- if (!checkTarEntrySafety(info)) {
273
- return false;
274
- }
275
- const relPath = stripArchivePath(info.path, strip);
285
+ const relPath = admission.consume(info);
276
286
  if (!relPath) {
277
287
  return false;
278
288
  }
279
- // Hand node-tar the exact path this entry was validated,
280
- // limited and collision-checked under, and that the mode
281
- // pass below resolves after extraction.
289
+ // Writes and the mode pass use the admitted output identity.
282
290
  entry.path = relPath;
283
291
  acceptedEntries.push({
284
292
  path: relPath,
@@ -306,8 +314,7 @@ export async function extractArchive(params) {
306
314
  }
307
315
  catch (err) {
308
316
  const error = err instanceof Error ? err : new Error(String(err));
309
- // Node's EventEmitter calls listeners with `this` bound to the
310
- // emitter (tar.Unpack), which exposes Parser.abort().
317
+ // EventEmitter binds `this` to tar.Unpack, exposing abort().
311
318
  const emitter = this;
312
319
  emitter.abort?.(error);
313
320
  }
@@ -321,19 +328,23 @@ export async function extractArchive(params) {
321
328
  catch (error) {
322
329
  throw normalizeTarParserError(createPipelineTimeoutError(error, deadline));
323
330
  }
331
+ admission.finish();
324
332
  for (const accepted of acceptedEntries) {
333
+ deadline.check();
325
334
  const outputPath = resolveArchiveOutputPath({
326
335
  rootDir: stagingDir,
327
336
  relPath: accepted.path,
328
337
  originalPath: accepted.path,
329
338
  });
330
339
  await fs.chmod(outputPath, accepted.mode);
340
+ deadline.check();
331
341
  }
332
342
  deadline.check();
333
343
  await mergeExtractedTreeIntoDestination({
334
344
  sourceDir: stagingDir,
335
345
  destinationDir: params.destDir,
336
346
  destinationRealDir,
347
+ deadline,
337
348
  });
338
349
  deadline.check();
339
350
  },
@@ -349,10 +360,10 @@ export async function extractArchive(params) {
349
360
  archivePath: params.archivePath,
350
361
  destDir: params.destDir,
351
362
  stripComponents: params.stripComponents,
352
- limits: params.limits,
363
+ limits,
353
364
  deadline,
354
365
  entryModes: params.entryModes,
355
366
  entryFilter: params.entryFilter,
356
- onFiltered: params.onFiltered,
367
+ onFiltered,
357
368
  }));
358
369
  }
package/dist/atomic.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export { replaceFileAtomic, replaceFileAtomicSync, type ReplaceFileAtomicFileSystem, type ReplaceFileAtomicOptions, type ReplaceFileAtomicRestoreCleanup, type ReplaceFileAtomicRestoreFailureDetails, type ReplaceFileAtomicResult, type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy, type ReplaceFileAtomicSyncFileSystem, type ReplaceFileAtomicSyncOptions, } from "./replace-file.js";
1
+ export { replaceFileAtomic, replaceFileAtomicSync, type RenameIdentityPolicy, type ReplaceFileAtomicFileSystem, type ReplaceFileAtomicOptions, type ReplaceFileAtomicRestoreCleanup, type ReplaceFileAtomicRestoreFailureDetails, type ReplaceFileAtomicResult, type ReplaceFileCopyFallbackRestorePolicy, type ReplaceFileDestinationHardlinkPolicy, type ReplaceFileAtomicSyncFileSystem, type ReplaceFileAtomicSyncOptions, } from "./replace-file.js";
2
2
  export { writeTextAtomic, type WriteTextAtomicOptions } from "./text-atomic.js";
3
3
  export { replaceDirectoryAtomic, type ReplaceDirectoryAtomicOptions } from "./replace-directory.js";
4
4
  export { movePathWithCopyFallback, type MovePathWithCopyFallbackOptions } from "./move-path.js";
@@ -1 +1 @@
1
- {"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,uBAAuB,EAC5B,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EACzC,KAAK,+BAA+B,EACpC,KAAK,4BAA4B,GAClC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,KAAK,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACpG,OAAO,EAAE,wBAAwB,EAAE,KAAK,+BAA+B,EAAE,MAAM,gBAAgB,CAAC"}
1
+ {"version":3,"file":"atomic.d.ts","sourceRoot":"","sources":["../src/atomic.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,iBAAiB,EACjB,qBAAqB,EACrB,KAAK,oBAAoB,EACzB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC7B,KAAK,+BAA+B,EACpC,KAAK,sCAAsC,EAC3C,KAAK,uBAAuB,EAC5B,KAAK,oCAAoC,EACzC,KAAK,oCAAoC,EACzC,KAAK,+BAA+B,EACpC,KAAK,4BAA4B,GAClC,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,eAAe,EAAE,KAAK,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAChF,OAAO,EAAE,sBAAsB,EAAE,KAAK,6BAA6B,EAAE,MAAM,wBAAwB,CAAC;AACpG,OAAO,EAAE,wBAAwB,EAAE,KAAK,+BAA+B,EAAE,MAAM,gBAAgB,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"bounded-read-stream.d.ts","sourceRoot":"","sources":["../src/bounded-read-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;AAGlD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAwBnE;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE;IAAE,MAAM,EAAE;QAAE,gBAAgB,IAAI,MAAM,CAAC,cAAc,CAAA;KAAE,CAAA;CAAE,EACjE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAAC,cAAc,CAWvB"}
1
+ {"version":3,"file":"bounded-read-stream.d.ts","sourceRoot":"","sources":["../src/bounded-read-stream.ts"],"names":[],"mappings":"AAAA,OAAO,EAAY,SAAS,EAAE,MAAM,aAAa,CAAC;AAIlD,wBAAgB,uBAAuB,CAAC,QAAQ,EAAE,MAAM,GAAG,SAAS,CAmBnE;AAED,wBAAgB,uBAAuB,CACrC,MAAM,EAAE;IAAE,MAAM,EAAE;QAAE,gBAAgB,IAAI,MAAM,CAAC,cAAc,CAAA;KAAE,CAAA;CAAE,EACjE,QAAQ,EAAE,MAAM,GAAG,SAAS,GAC3B,MAAM,CAAC,cAAc,CAWvB"}
@@ -1,10 +1,8 @@
1
1
  import { pipeline, Transform } from "node:stream";
2
+ import { normalizeMaxBytes } from "./byte-budget.js";
2
3
  import { FsSafeError } from "./errors.js";
3
4
  export function createMaxBytesTransform(maxBytes) {
4
- if (maxBytes !== Number.POSITIVE_INFINITY &&
5
- (!Number.isSafeInteger(maxBytes) || maxBytes < 0)) {
6
- throw new RangeError("maxBytes must be a non-negative safe integer or Infinity");
7
- }
5
+ normalizeMaxBytes(maxBytes);
8
6
  let bytes = 0;
9
7
  return new Transform({
10
8
  transform(chunk, _encoding, callback) {