@openclaw/fs-safe 0.6.0 → 0.7.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 (183) hide show
  1. package/CHANGELOG.md +50 -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 -26
  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/file-lock-sync.d.ts.map +1 -1
  57. package/dist/file-lock-sync.js +10 -5
  58. package/dist/file-store-boundary.d.ts.map +1 -1
  59. package/dist/file-store-boundary.js +9 -5
  60. package/dist/file-store-limit.d.ts +2 -0
  61. package/dist/file-store-limit.d.ts.map +1 -0
  62. package/dist/file-store-limit.js +8 -0
  63. package/dist/file-store-sync-write.d.ts.map +1 -1
  64. package/dist/file-store-sync-write.js +38 -8
  65. package/dist/file-store.d.ts.map +1 -1
  66. package/dist/file-store.js +26 -26
  67. package/dist/json-durable-queue-directory.d.ts +2 -0
  68. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  69. package/dist/json-durable-queue-directory.js +20 -0
  70. package/dist/json-durable-queue-ownership.d.ts +14 -0
  71. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  72. package/dist/json-durable-queue-ownership.js +168 -0
  73. package/dist/json-durable-queue-retirement.d.ts +9 -0
  74. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  75. package/dist/json-durable-queue-retirement.js +126 -0
  76. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  77. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  78. package/dist/json-durable-queue-transfer-lock.js +19 -0
  79. package/dist/json-durable-queue.d.ts +1 -0
  80. package/dist/json-durable-queue.d.ts.map +1 -1
  81. package/dist/json-durable-queue.js +90 -57
  82. package/dist/json.d.ts.map +1 -1
  83. package/dist/json.js +27 -8
  84. package/dist/local-roots.d.ts.map +1 -1
  85. package/dist/local-roots.js +4 -2
  86. package/dist/native-binding.d.ts +12 -3
  87. package/dist/native-binding.d.ts.map +1 -1
  88. package/dist/output-sibling.d.ts.map +1 -1
  89. package/dist/output-sibling.js +11 -110
  90. package/dist/output.d.ts.map +1 -1
  91. package/dist/output.js +4 -2
  92. package/dist/pinned-open.d.ts.map +1 -1
  93. package/dist/pinned-open.js +27 -48
  94. package/dist/pinned-write.d.ts.map +1 -1
  95. package/dist/pinned-write.js +8 -9
  96. package/dist/private-temp-workspace.d.ts +3 -1
  97. package/dist/private-temp-workspace.d.ts.map +1 -1
  98. package/dist/private-temp-workspace.js +81 -56
  99. package/dist/publish-file.d.ts.map +1 -1
  100. package/dist/publish-file.js +2 -4
  101. package/dist/read-opened-file.d.ts.map +1 -1
  102. package/dist/read-opened-file.js +6 -4
  103. package/dist/regular-file.d.ts.map +1 -1
  104. package/dist/regular-file.js +136 -82
  105. package/dist/replace-file-copy-fallback.d.ts +3 -1
  106. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  107. package/dist/replace-file-copy-fallback.js +21 -36
  108. package/dist/replace-file-copy-source.d.ts +21 -0
  109. package/dist/replace-file-copy-source.d.ts.map +1 -0
  110. package/dist/replace-file-copy-source.js +112 -0
  111. package/dist/replace-file-descriptor.d.ts +13 -3
  112. package/dist/replace-file-descriptor.d.ts.map +1 -1
  113. package/dist/replace-file-descriptor.js +32 -7
  114. package/dist/replace-file-rename-policy.d.ts +7 -0
  115. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  116. package/dist/replace-file-rename-policy.js +30 -0
  117. package/dist/replace-file-temp-owner.d.ts +46 -0
  118. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  119. package/dist/replace-file-temp-owner.js +346 -0
  120. package/dist/replace-file.d.ts +6 -1
  121. package/dist/replace-file.d.ts.map +1 -1
  122. package/dist/replace-file.js +72 -58
  123. package/dist/root-impl.d.ts.map +1 -1
  124. package/dist/root-impl.js +40 -26
  125. package/dist/secret-file.d.ts.map +1 -1
  126. package/dist/secret-file.js +6 -3
  127. package/dist/secret-read-async.d.ts.map +1 -1
  128. package/dist/secret-read-async.js +6 -5
  129. package/dist/secure-file.d.ts.map +1 -1
  130. package/dist/secure-file.js +17 -13
  131. package/dist/sibling-staged-file.d.ts +15 -0
  132. package/dist/sibling-staged-file.d.ts.map +1 -0
  133. package/dist/sibling-staged-file.js +148 -0
  134. package/dist/sibling-temp.d.ts +3 -0
  135. package/dist/sibling-temp.d.ts.map +1 -1
  136. package/dist/sibling-temp.js +30 -74
  137. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  138. package/dist/sidecar-lock-acquire.js +48 -27
  139. package/dist/sidecar-lock-handle.d.ts +6 -2
  140. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  141. package/dist/sidecar-lock-handle.js +17 -3
  142. package/dist/sidecar-lock-policy.d.ts +2 -0
  143. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  144. package/dist/sidecar-lock-policy.js +29 -0
  145. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  146. package/dist/sidecar-lock-reclaim.js +29 -7
  147. package/dist/sidecar-lock.d.ts.map +1 -1
  148. package/dist/sidecar-lock.js +28 -16
  149. package/dist/suppressed-error.d.ts +6 -0
  150. package/dist/suppressed-error.d.ts.map +1 -0
  151. package/dist/suppressed-error.js +15 -0
  152. package/dist/temp-cleanup.d.ts +2 -0
  153. package/dist/temp-cleanup.d.ts.map +1 -1
  154. package/dist/temp-cleanup.js +25 -10
  155. package/dist/temp-workspace-owner.d.ts +23 -0
  156. package/dist/temp-workspace-owner.d.ts.map +1 -0
  157. package/dist/temp-workspace-owner.js +320 -0
  158. package/dist/temp.d.ts +1 -1
  159. package/dist/temp.d.ts.map +1 -1
  160. package/dist/test-hooks.d.ts +5 -0
  161. package/dist/test-hooks.d.ts.map +1 -1
  162. package/docs/advanced.md +8 -1
  163. package/docs/archive.md +194 -26
  164. package/docs/atomic.md +11 -2
  165. package/docs/durability.md +1 -1
  166. package/docs/file-store.md +2 -0
  167. package/docs/install.md +8 -4
  168. package/docs/json.md +8 -4
  169. package/docs/native-helper.md +15 -3
  170. package/docs/native.md +33 -6
  171. package/docs/output.md +10 -0
  172. package/docs/public-api.md +6 -3
  173. package/docs/quickstart.md +1 -1
  174. package/docs/reading.md +1 -1
  175. package/docs/regular-file.md +9 -2
  176. package/docs/root.md +2 -0
  177. package/docs/secret-file.md +5 -1
  178. package/docs/secure-file.md +4 -2
  179. package/docs/security-model.md +18 -0
  180. package/docs/sidecar-lock.md +4 -0
  181. package/docs/store.md +13 -0
  182. package/docs/temp.md +128 -14
  183. 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;AA0NpC,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,
@@ -151,7 +156,7 @@ async function extractZip(params) {
151
156
  if (!shouldExtractArchiveEntry({
152
157
  filter: params.entryFilter,
153
158
  onFiltered: params.onFiltered,
154
- entry: { path: entry.name, kind: entryKind, size: entrySize },
159
+ entry: { path: output.canonicalPath, kind: entryKind, size: entrySize },
155
160
  })) {
156
161
  continue;
157
162
  }
@@ -166,6 +171,7 @@ async function extractZip(params) {
166
171
  outPath: output.outPath,
167
172
  originalPath: entry.name,
168
173
  isDirectory: entry.dir,
174
+ deadline: params.deadline,
169
175
  });
170
176
  if (entry.dir) {
171
177
  await fs.chmod(output.outPath, mode);
@@ -184,6 +190,7 @@ async function extractZip(params) {
184
190
  sourceDir: stagingRealDir,
185
191
  destinationDir: params.destDir,
186
192
  destinationRealDir,
193
+ deadline: params.deadline,
187
194
  });
188
195
  params.deadline.check();
189
196
  },
@@ -194,11 +201,14 @@ async function extractZip(params) {
194
201
  }
195
202
  }
196
203
  export async function extractArchive(params) {
204
+ const onFiltered = resolveArchiveFilteredEntryPolicy(params.onFiltered);
197
205
  const kind = params.kind ?? resolveArchiveKind(params.archivePath);
198
206
  if (!kind) {
199
207
  throw new Error(`unsupported archive: ${params.archivePath}`);
200
208
  }
201
209
  const label = kind === "zip" ? "extract zip" : "extract tar";
210
+ const limits = resolveExtractLimits(params.limits);
211
+ const tarLimits = resolveTarMeterLimits(limits);
202
212
  const native = getNativeBinding();
203
213
  if (native) {
204
214
  await withExtractionDeadline(params.timeoutMs, label, async (deadline) => extractNativeArchive({
@@ -207,11 +217,12 @@ export async function extractArchive(params) {
207
217
  destDir: params.destDir,
208
218
  kind,
209
219
  stripComponents: params.stripComponents,
210
- limits: params.limits,
220
+ limits,
221
+ tarLimits,
211
222
  deadline,
212
223
  entryModes: params.entryModes,
213
224
  entryFilter: params.entryFilter,
214
- onFiltered: params.onFiltered,
225
+ onFiltered,
215
226
  }));
216
227
  return;
217
228
  }
@@ -222,17 +233,18 @@ export async function extractArchive(params) {
222
233
  if (kind === "tar") {
223
234
  await withExtractionDeadline(params.timeoutMs, label, async (deadline) => {
224
235
  const tar = await importOptionalTar();
225
- const limits = resolveExtractLimits(params.limits);
226
236
  const stagedArchive = await stageArchiveFileForExtraction({
227
237
  archivePath: params.archivePath,
228
238
  limits,
229
239
  deadline,
230
240
  });
231
241
  try {
242
+ const manifest = [];
232
243
  await preflightTarMetadata({
233
244
  archivePath: stagedArchive.path,
234
- maxMetaEntryBytes: limits.maxMetaEntryBytes,
245
+ limits: tarLimits,
235
246
  signal: deadline.signal,
247
+ onMember: (entry) => { manifest.push(entry); },
236
248
  });
237
249
  deadline.check();
238
250
  const destinationRealDir = await prepareArchiveDestinationDir(params.destDir);
@@ -246,13 +258,14 @@ export async function extractArchive(params) {
246
258
  stripComponents: params.stripComponents,
247
259
  limits,
248
260
  entryFilter: params.entryFilter,
249
- onFiltered: params.onFiltered,
261
+ onFiltered,
250
262
  });
263
+ const admission = createTarAdmissionPlan(manifest, (entry) => {
264
+ deadline.check();
265
+ return checkTarEntrySafety(entry);
266
+ }, strip);
251
267
  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.
268
+ // Extract privately, then merge through the safe-open boundary.
256
269
  const extractor = tar.x({
257
270
  cwd: stagingDir,
258
271
  // fs-safe owns stripping: node-tar counts the `.` and empty path
@@ -265,20 +278,16 @@ export async function extractArchive(params) {
265
278
  preserveOwner: false,
266
279
  noMtime: true,
267
280
  strict: true,
268
- maxMetaEntrySize: limits.maxMetaEntryBytes,
281
+ maxMetaEntrySize: tarLimits.maxMetaEntryBytes,
282
+ maxDecompressionRatio: NODE_TAR_RATIO_DISABLED,
269
283
  filter(_entryPath, entry) {
270
284
  try {
271
285
  const info = readTarEntryInfo(entry);
272
- if (!checkTarEntrySafety(info)) {
273
- return false;
274
- }
275
- const relPath = stripArchivePath(info.path, strip);
286
+ const relPath = admission.consume(info);
276
287
  if (!relPath) {
277
288
  return false;
278
289
  }
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.
290
+ // Writes and the mode pass use the admitted output identity.
282
291
  entry.path = relPath;
283
292
  acceptedEntries.push({
284
293
  path: relPath,
@@ -306,8 +315,7 @@ export async function extractArchive(params) {
306
315
  }
307
316
  catch (err) {
308
317
  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().
318
+ // EventEmitter binds `this` to tar.Unpack, exposing abort().
311
319
  const emitter = this;
312
320
  emitter.abort?.(error);
313
321
  }
@@ -321,19 +329,23 @@ export async function extractArchive(params) {
321
329
  catch (error) {
322
330
  throw normalizeTarParserError(createPipelineTimeoutError(error, deadline));
323
331
  }
332
+ admission.finish();
324
333
  for (const accepted of acceptedEntries) {
334
+ deadline.check();
325
335
  const outputPath = resolveArchiveOutputPath({
326
336
  rootDir: stagingDir,
327
337
  relPath: accepted.path,
328
338
  originalPath: accepted.path,
329
339
  });
330
340
  await fs.chmod(outputPath, accepted.mode);
341
+ deadline.check();
331
342
  }
332
343
  deadline.check();
333
344
  await mergeExtractedTreeIntoDestination({
334
345
  sourceDir: stagingDir,
335
346
  destinationDir: params.destDir,
336
347
  destinationRealDir,
348
+ deadline,
337
349
  });
338
350
  deadline.check();
339
351
  },
@@ -349,10 +361,10 @@ export async function extractArchive(params) {
349
361
  archivePath: params.archivePath,
350
362
  destDir: params.destDir,
351
363
  stripComponents: params.stripComponents,
352
- limits: params.limits,
364
+ limits,
353
365
  deadline,
354
366
  entryModes: params.entryModes,
355
367
  entryFilter: params.entryFilter,
356
- onFiltered: params.onFiltered,
368
+ onFiltered,
357
369
  }));
358
370
  }
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) {
@@ -1 +1 @@
1
- {"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAKnD,KAAK,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AA4DnD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAIjB;AAcD,oFAAoF;AACpF,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7F;AAED,mFAAmF;AACnF,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAalF"}
1
+ {"version":3,"file":"bounded-read.d.ts","sourceRoot":"","sources":["../src/bounded-read.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAMnD,KAAK,kBAAkB,GAAG,IAAI,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;AAmDnD;;;;GAIG;AACH,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,kBAAkB,EAC1B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,MAAM,CAAC,CAIjB;AAcD,oFAAoF;AACpF,wBAAsB,yBAAyB,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI7F;AAED,mFAAmF;AACnF,wBAAgB,6BAA6B,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAalF"}