@openclaw/fs-safe 0.5.6 → 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 (289) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +23 -10
  3. package/dist/advanced.d.ts +2 -1
  4. package/dist/advanced.d.ts.map +1 -1
  5. package/dist/advanced.js +1 -0
  6. package/dist/archive-crc32.d.ts +2 -0
  7. package/dist/archive-crc32.d.ts.map +1 -0
  8. package/dist/archive-crc32.js +14 -0
  9. package/dist/archive-deadline.d.ts +3 -0
  10. package/dist/archive-deadline.d.ts.map +1 -1
  11. package/dist/archive-deadline.js +44 -8
  12. package/dist/archive-entry.d.ts.map +1 -1
  13. package/dist/archive-entry.js +1 -0
  14. package/dist/archive-errors.d.ts +1 -0
  15. package/dist/archive-errors.d.ts.map +1 -1
  16. package/dist/archive-errors.js +3 -0
  17. package/dist/archive-input.d.ts.map +1 -1
  18. package/dist/archive-input.js +26 -16
  19. package/dist/archive-kind.js +2 -2
  20. package/dist/archive-limits.d.ts +11 -3
  21. package/dist/archive-limits.d.ts.map +1 -1
  22. package/dist/archive-limits.js +24 -0
  23. package/dist/archive-native.d.ts +3 -2
  24. package/dist/archive-native.d.ts.map +1 -1
  25. package/dist/archive-native.js +31 -9
  26. package/dist/archive-policy.d.ts +2 -0
  27. package/dist/archive-policy.d.ts.map +1 -1
  28. package/dist/archive-policy.js +9 -1
  29. package/dist/archive-read.d.ts.map +1 -1
  30. package/dist/archive-read.js +88 -41
  31. package/dist/archive-staging.d.ts +3 -0
  32. package/dist/archive-staging.d.ts.map +1 -1
  33. package/dist/archive-staging.js +91 -43
  34. package/dist/archive-tar-admission.d.ts +7 -0
  35. package/dist/archive-tar-admission.d.ts.map +1 -0
  36. package/dist/archive-tar-admission.js +43 -0
  37. package/dist/archive-tar-gnu.d.ts +2 -0
  38. package/dist/archive-tar-gnu.d.ts.map +1 -0
  39. package/dist/archive-tar-gnu.js +20 -0
  40. package/dist/archive-tar-header.d.ts +8 -0
  41. package/dist/archive-tar-header.d.ts.map +1 -0
  42. package/dist/archive-tar-header.js +47 -0
  43. package/dist/archive-tar-meta.d.ts +29 -1
  44. package/dist/archive-tar-meta.d.ts.map +1 -1
  45. package/dist/archive-tar-meta.js +135 -19
  46. package/dist/archive-tar-pax.d.ts +8 -0
  47. package/dist/archive-tar-pax.d.ts.map +1 -0
  48. package/dist/archive-tar-pax.js +100 -0
  49. package/dist/archive-tar-runtime.d.ts +4 -0
  50. package/dist/archive-tar-runtime.d.ts.map +1 -1
  51. package/dist/archive-tar-runtime.js +3 -0
  52. package/dist/archive-tar.d.ts.map +1 -1
  53. package/dist/archive-tar.js +8 -2
  54. package/dist/archive-zip-admission.d.ts +7 -0
  55. package/dist/archive-zip-admission.d.ts.map +1 -0
  56. package/dist/archive-zip-admission.js +60 -0
  57. package/dist/archive-zip-count.d.ts +2 -0
  58. package/dist/archive-zip-count.d.ts.map +1 -0
  59. package/dist/archive-zip-count.js +140 -0
  60. package/dist/archive-zip-directory.d.ts +8 -0
  61. package/dist/archive-zip-directory.d.ts.map +1 -0
  62. package/dist/archive-zip-directory.js +224 -0
  63. package/dist/archive-zip-integrity.d.ts.map +1 -1
  64. package/dist/archive-zip-integrity.js +1 -14
  65. package/dist/archive-zip-names.d.ts +12 -0
  66. package/dist/archive-zip-names.d.ts.map +1 -0
  67. package/dist/archive-zip-names.js +99 -0
  68. package/dist/archive-zip-preflight.d.ts +1 -1
  69. package/dist/archive-zip-preflight.d.ts.map +1 -1
  70. package/dist/archive-zip-preflight.js +8 -145
  71. package/dist/archive.d.ts.map +1 -1
  72. package/dist/archive.js +63 -42
  73. package/dist/atomic.d.ts +1 -1
  74. package/dist/atomic.d.ts.map +1 -1
  75. package/dist/bounded-read-stream.d.ts.map +1 -1
  76. package/dist/bounded-read-stream.js +2 -4
  77. package/dist/bounded-read.d.ts.map +1 -1
  78. package/dist/bounded-read.js +3 -10
  79. package/dist/byte-budget.d.ts +5 -0
  80. package/dist/byte-budget.d.ts.map +1 -0
  81. package/dist/byte-budget.js +9 -0
  82. package/dist/file-hash.d.ts.map +1 -1
  83. package/dist/file-hash.js +25 -19
  84. package/dist/file-lock-sync.d.ts.map +1 -1
  85. package/dist/file-lock-sync.js +26 -9
  86. package/dist/file-store-boundary.d.ts.map +1 -1
  87. package/dist/file-store-boundary.js +9 -5
  88. package/dist/file-store-limit.d.ts +2 -0
  89. package/dist/file-store-limit.d.ts.map +1 -0
  90. package/dist/file-store-limit.js +8 -0
  91. package/dist/file-store-sync-write.d.ts.map +1 -1
  92. package/dist/file-store-sync-write.js +38 -8
  93. package/dist/file-store.d.ts.map +1 -1
  94. package/dist/file-store.js +26 -26
  95. package/dist/json-durable-queue-directory.d.ts +2 -0
  96. package/dist/json-durable-queue-directory.d.ts.map +1 -0
  97. package/dist/json-durable-queue-directory.js +20 -0
  98. package/dist/json-durable-queue-ownership.d.ts +14 -0
  99. package/dist/json-durable-queue-ownership.d.ts.map +1 -0
  100. package/dist/json-durable-queue-ownership.js +168 -0
  101. package/dist/json-durable-queue-retirement.d.ts +9 -0
  102. package/dist/json-durable-queue-retirement.d.ts.map +1 -0
  103. package/dist/json-durable-queue-retirement.js +126 -0
  104. package/dist/json-durable-queue-transfer-lock.d.ts +2 -0
  105. package/dist/json-durable-queue-transfer-lock.d.ts.map +1 -0
  106. package/dist/json-durable-queue-transfer-lock.js +19 -0
  107. package/dist/json-durable-queue.d.ts +1 -0
  108. package/dist/json-durable-queue.d.ts.map +1 -1
  109. package/dist/json-durable-queue.js +90 -57
  110. package/dist/json.d.ts.map +1 -1
  111. package/dist/json.js +27 -8
  112. package/dist/local-roots.d.ts.map +1 -1
  113. package/dist/local-roots.js +4 -2
  114. package/dist/native-binding.d.ts +15 -3
  115. package/dist/native-binding.d.ts.map +1 -1
  116. package/dist/native-operations.d.ts +4 -1
  117. package/dist/native-operations.d.ts.map +1 -1
  118. package/dist/native-operations.js +22 -6
  119. package/dist/native-pinned-write-windows.d.ts +8 -0
  120. package/dist/native-pinned-write-windows.d.ts.map +1 -0
  121. package/dist/native-pinned-write-windows.js +92 -0
  122. package/dist/native-pinned-write.d.ts.map +1 -1
  123. package/dist/native-pinned-write.js +136 -127
  124. package/dist/native-staged-file.d.ts +24 -0
  125. package/dist/native-staged-file.d.ts.map +1 -0
  126. package/dist/native-staged-file.js +337 -0
  127. package/dist/native.d.ts.map +1 -1
  128. package/dist/native.js +4 -4
  129. package/dist/opened-realpath.d.ts +2 -0
  130. package/dist/opened-realpath.d.ts.map +1 -1
  131. package/dist/opened-realpath.js +12 -7
  132. package/dist/output-sibling.d.ts.map +1 -1
  133. package/dist/output-sibling.js +11 -110
  134. package/dist/output.d.ts.map +1 -1
  135. package/dist/output.js +4 -2
  136. package/dist/owner-dacl.d.ts.map +1 -1
  137. package/dist/owner-dacl.js +2 -1
  138. package/dist/permission-exec.d.ts +19 -0
  139. package/dist/permission-exec.d.ts.map +1 -1
  140. package/dist/permission-exec.js +57 -11
  141. package/dist/permissions-public.d.ts +1 -1
  142. package/dist/permissions-public.d.ts.map +1 -1
  143. package/dist/permissions-windows.d.ts +3 -0
  144. package/dist/permissions-windows.d.ts.map +1 -1
  145. package/dist/permissions-windows.js +16 -5
  146. package/dist/permissions.d.ts +5 -0
  147. package/dist/permissions.d.ts.map +1 -1
  148. package/dist/pinned-open.d.ts.map +1 -1
  149. package/dist/pinned-open.js +27 -48
  150. package/dist/pinned-write.d.ts +6 -0
  151. package/dist/pinned-write.d.ts.map +1 -1
  152. package/dist/pinned-write.js +26 -37
  153. package/dist/private-directory.d.ts.map +1 -1
  154. package/dist/private-directory.js +3 -2
  155. package/dist/private-temp-workspace.d.ts +3 -1
  156. package/dist/private-temp-workspace.d.ts.map +1 -1
  157. package/dist/private-temp-workspace.js +81 -56
  158. package/dist/publish-file.d.ts.map +1 -1
  159. package/dist/publish-file.js +2 -4
  160. package/dist/read-opened-file.d.ts.map +1 -1
  161. package/dist/read-opened-file.js +6 -4
  162. package/dist/regular-file.d.ts.map +1 -1
  163. package/dist/regular-file.js +136 -82
  164. package/dist/replace-file-copy-fallback.d.ts +3 -1
  165. package/dist/replace-file-copy-fallback.d.ts.map +1 -1
  166. package/dist/replace-file-copy-fallback.js +21 -36
  167. package/dist/replace-file-copy-source.d.ts +21 -0
  168. package/dist/replace-file-copy-source.d.ts.map +1 -0
  169. package/dist/replace-file-copy-source.js +112 -0
  170. package/dist/replace-file-descriptor.d.ts +13 -3
  171. package/dist/replace-file-descriptor.d.ts.map +1 -1
  172. package/dist/replace-file-descriptor.js +32 -7
  173. package/dist/replace-file-rename-policy.d.ts +7 -0
  174. package/dist/replace-file-rename-policy.d.ts.map +1 -0
  175. package/dist/replace-file-rename-policy.js +30 -0
  176. package/dist/replace-file-temp-owner.d.ts +46 -0
  177. package/dist/replace-file-temp-owner.d.ts.map +1 -0
  178. package/dist/replace-file-temp-owner.js +346 -0
  179. package/dist/replace-file.d.ts +6 -1
  180. package/dist/replace-file.d.ts.map +1 -1
  181. package/dist/replace-file.js +72 -58
  182. package/dist/root-impl.d.ts.map +1 -1
  183. package/dist/root-impl.js +118 -98
  184. package/dist/root-paths.d.ts +11 -14
  185. package/dist/root-paths.d.ts.map +1 -1
  186. package/dist/root-paths.js +36 -27
  187. package/dist/root-write-verification.d.ts +11 -0
  188. package/dist/root-write-verification.d.ts.map +1 -0
  189. package/dist/root-write-verification.js +91 -0
  190. package/dist/secret-file.d.ts +1 -6
  191. package/dist/secret-file.d.ts.map +1 -1
  192. package/dist/secret-file.js +49 -120
  193. package/dist/secret-read-async.d.ts +1 -1
  194. package/dist/secret-read-async.d.ts.map +1 -1
  195. package/dist/secret-read-async.js +51 -72
  196. package/dist/secret-read-policy.d.ts +13 -0
  197. package/dist/secret-read-policy.d.ts.map +1 -0
  198. package/dist/secret-read-policy.js +28 -0
  199. package/dist/secret.d.ts +2 -1
  200. package/dist/secret.d.ts.map +1 -1
  201. package/dist/secret.js +2 -1
  202. package/dist/secure-file.d.ts.map +1 -1
  203. package/dist/secure-file.js +42 -29
  204. package/dist/sibling-staged-file.d.ts +15 -0
  205. package/dist/sibling-staged-file.d.ts.map +1 -0
  206. package/dist/sibling-staged-file.js +148 -0
  207. package/dist/sibling-temp.d.ts +3 -0
  208. package/dist/sibling-temp.d.ts.map +1 -1
  209. package/dist/sibling-temp.js +30 -74
  210. package/dist/sidecar-lock-acquire.d.ts.map +1 -1
  211. package/dist/sidecar-lock-acquire.js +48 -27
  212. package/dist/sidecar-lock-handle.d.ts +6 -2
  213. package/dist/sidecar-lock-handle.d.ts.map +1 -1
  214. package/dist/sidecar-lock-handle.js +17 -3
  215. package/dist/sidecar-lock-policy.d.ts +2 -0
  216. package/dist/sidecar-lock-policy.d.ts.map +1 -1
  217. package/dist/sidecar-lock-policy.js +29 -0
  218. package/dist/sidecar-lock-reclaim.d.ts.map +1 -1
  219. package/dist/sidecar-lock-reclaim.js +29 -7
  220. package/dist/sidecar-lock.d.ts.map +1 -1
  221. package/dist/sidecar-lock.js +28 -16
  222. package/dist/staged-directory.d.ts +16 -0
  223. package/dist/staged-directory.d.ts.map +1 -0
  224. package/dist/staged-directory.js +60 -0
  225. package/dist/staged-file-types.d.ts +56 -0
  226. package/dist/staged-file-types.d.ts.map +1 -0
  227. package/dist/staged-file-types.js +1 -0
  228. package/dist/staged-file.d.ts +10 -0
  229. package/dist/staged-file.d.ts.map +1 -0
  230. package/dist/staged-file.js +15 -0
  231. package/dist/strict-file-identity.d.ts +6 -0
  232. package/dist/strict-file-identity.d.ts.map +1 -0
  233. package/dist/strict-file-identity.js +48 -0
  234. package/dist/suppressed-error.d.ts +6 -0
  235. package/dist/suppressed-error.d.ts.map +1 -0
  236. package/dist/suppressed-error.js +15 -0
  237. package/dist/temp-cleanup.d.ts +2 -0
  238. package/dist/temp-cleanup.d.ts.map +1 -1
  239. package/dist/temp-cleanup.js +25 -10
  240. package/dist/temp-workspace-owner.d.ts +23 -0
  241. package/dist/temp-workspace-owner.d.ts.map +1 -0
  242. package/dist/temp-workspace-owner.js +320 -0
  243. package/dist/temp.d.ts +1 -1
  244. package/dist/temp.d.ts.map +1 -1
  245. package/dist/test-hooks.d.ts +5 -0
  246. package/dist/test-hooks.d.ts.map +1 -1
  247. package/dist/windows-owner.d.ts +3 -0
  248. package/dist/windows-owner.d.ts.map +1 -1
  249. package/dist/windows-owner.js +10 -2
  250. package/docs/advanced.md +19 -2
  251. package/docs/archive.md +250 -35
  252. package/docs/atomic.md +11 -2
  253. package/docs/config.md +7 -0
  254. package/docs/contributing.md +45 -7
  255. package/docs/durability.md +18 -5
  256. package/docs/errors.md +16 -1
  257. package/docs/file-store.md +2 -0
  258. package/docs/index.md +3 -1
  259. package/docs/install.md +20 -8
  260. package/docs/json.md +8 -4
  261. package/docs/migrating-to-0.5.md +7 -7
  262. package/docs/migrating-to-0.6.md +43 -0
  263. package/docs/native-helper.md +30 -8
  264. package/docs/native.md +73 -17
  265. package/docs/output.md +10 -0
  266. package/docs/path-scope.md +28 -2
  267. package/docs/permissions.md +13 -2
  268. package/docs/public-api.md +6 -3
  269. package/docs/quickstart.md +1 -1
  270. package/docs/reading.md +1 -1
  271. package/docs/regular-file.md +9 -2
  272. package/docs/root.md +3 -1
  273. package/docs/secret-file.md +12 -0
  274. package/docs/secure-file.md +21 -3
  275. package/docs/security-model.md +33 -1
  276. package/docs/sidecar-lock.md +15 -1
  277. package/docs/staged-file.md +178 -0
  278. package/docs/store.md +13 -0
  279. package/docs/temp.md +128 -14
  280. package/docs/testing.md +1 -1
  281. package/docs/writing.md +19 -0
  282. package/package.json +16 -9
  283. package/dist/native/darwin-arm64/fs-safe-native.node +0 -0
  284. package/dist/native/darwin-x64/fs-safe-native.node +0 -0
  285. package/dist/native/linux-arm64-gnu/fs-safe-native.node +0 -0
  286. package/dist/native/linux-arm64-musl/fs-safe-native.node +0 -0
  287. package/dist/native/linux-x64-gnu/fs-safe-native.node +0 -0
  288. package/dist/native/linux-x64-musl/fs-safe-native.node +0 -0
  289. package/dist/native/win32-x64-msvc/fs-safe-native.node +0 -0
@@ -3,19 +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";
7
- class TarMetadataMeter extends Transform {
8
- maxMetaEntryBytes;
6
+ import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError, tarManifestEntryCost } from "./archive-limits.js";
7
+ import { parseLocalPax, paxMemberSize } from "./archive-tar-pax.js";
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;
9
15
  block = Buffer.alloc(512);
10
16
  blockLength = 0;
11
17
  state = { kind: "header" };
12
- constructor(maxMetaEntryBytes) {
18
+ pendingPax;
19
+ pendingGnu = new Set();
20
+ pendingGnuPath;
21
+ zeroBlocks = 0;
22
+ entries = 0;
23
+ remainingDecodedBytes;
24
+ manifestBytes = 0;
25
+ constructor(limits, onMember) {
13
26
  super();
14
- 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;
15
39
  }
16
40
  invalid(message) {
17
41
  return new ArchiveFormatError(`invalid TAR header: ${message}`);
18
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
+ }
19
49
  parseSize() {
20
50
  const field = this.block.subarray(124, 136);
21
51
  if ((field[0] ?? 0) & 0x80) {
@@ -32,7 +62,13 @@ class TarMetadataMeter extends Transform {
32
62
  return Number(value);
33
63
  }
34
64
  const zero = field.indexOf(0);
35
- const text = field.subarray(0, zero < 0 ? field.length : zero).toString("ascii").trim();
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
+ }
68
+ const bytes = field.subarray(0, zero < 0 ? field.length : zero);
69
+ if (bytes.some((byte) => byte > 0x7f))
70
+ throw this.invalid("size is not ASCII octal");
71
+ const text = bytes.toString("ascii").replace(/^ +| +$/g, "");
36
72
  if (!text || !/^[0-7]+$/.test(text))
37
73
  throw this.invalid("size is not valid octal");
38
74
  const value = Number.parseInt(text, 8);
@@ -42,29 +78,58 @@ class TarMetadataMeter extends Transform {
42
78
  }
43
79
  finishHeader() {
44
80
  if (this.block.every((byte) => byte === 0)) {
81
+ if (this.pendingPax)
82
+ throw this.invalid("dangling PAX metadata");
83
+ if (this.pendingGnu.size)
84
+ throw this.invalid("dangling GNU metadata");
45
85
  this.blockLength = 0;
46
- this.state = { kind: "header" };
86
+ this.zeroBlocks += 1;
87
+ this.state = { kind: this.zeroBlocks === 2 ? "eof" : "header" };
47
88
  return;
48
89
  }
90
+ if (this.zeroBlocks !== 0)
91
+ throw this.invalid("nonzero header after one TAR zero block");
92
+ validateTarHeader(this.block);
49
93
  const nameEnd = this.block.subarray(0, 100).indexOf(0);
50
94
  const name = this.block.subarray(0, nameEnd < 0 ? 100 : nameEnd);
51
95
  if (name.length === 0) {
52
96
  throw this.invalid("entry path is empty");
53
97
  }
54
- if (this.block[156] !== 0x35 && name.at(-1) === 0x2f) {
98
+ if (![0x35, 0x44].includes(this.block[156]) && name.at(-1) === 0x2f) {
55
99
  throw this.invalid("non-directory entry path ends with a separator");
56
100
  }
57
- const size = this.parseSize();
101
+ let size = this.parseSize();
102
+ let padded = this.paddedSize(size);
58
103
  const type = this.block[156];
59
- if ([0x78, 0x67, 0x4c, 0x4b, 0x58].includes(type ?? -1) && 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) {
60
109
  throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.META_ENTRY_SIZE_EXCEEDS_LIMIT);
61
110
  }
62
- if (type === 0x78 || type === 0x67 || type === 0x58) {
63
- throw this.invalid("PAX metadata is unmeterable without interpreting content");
111
+ if (type === 0x67 || type === 0x58 || type === 0x4e) {
112
+ throw this.invalid("global/old PAX and old GNU metadata are not supported");
64
113
  }
65
- const padded = Math.ceil(size / 512) * 512;
66
- if (!Number.isSafeInteger(padded))
67
- throw this.invalid("entry padding exceeds the safe integer range");
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
+ }
119
+ const magic = this.block.subarray(257, 265).toString("latin1");
120
+ if (magic !== "ustar\0" + "00" && magic !== "ustar \0")
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 };
125
+ this.blockLength = 0;
126
+ return;
127
+ }
128
+ // Sparse headers retain their metadata-limit-before-format-error ordering.
129
+ if (this.pendingPax && type !== 0x53) {
130
+ size = paxMemberSize(this.pendingPax, type, size, this.block);
131
+ }
132
+ padded = this.paddedSize(size);
68
133
  if (type === 0x53) {
69
134
  if (this.block[482] !== 0 && this.block[482] !== 1) {
70
135
  throw this.invalid("GNU sparse extension flag is not 0 or 1");
@@ -75,13 +140,27 @@ class TarMetadataMeter extends Transform {
75
140
  this.state = { kind: "sparse", dataRemaining: padded, metaBytes: 0 };
76
141
  }
77
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;
78
157
  this.state = padded === 0 ? { kind: "header" } : { kind: "data", remaining: padded };
79
158
  }
80
159
  this.blockLength = 0;
81
160
  }
82
161
  finishSparseHeader(state) {
83
162
  const metaBytes = state.metaBytes + 512;
84
- if (metaBytes > this.maxMetaEntryBytes) {
163
+ if (metaBytes > this.limits.maxMetaEntryBytes) {
85
164
  throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.META_ENTRY_SIZE_EXCEEDS_LIMIT);
86
165
  }
87
166
  if (this.block[504] !== 0 && this.block[504] !== 1) {
@@ -98,6 +177,29 @@ class TarMetadataMeter extends Transform {
98
177
  meter(chunk) {
99
178
  let offset = 0;
100
179
  while (offset < chunk.length) {
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") {
186
+ const state = this.state;
187
+ const take = Math.min(state.body.length - state.used, chunk.length - offset);
188
+ chunk.copy(state.body, state.used, offset, offset + take);
189
+ state.used += take;
190
+ offset += take;
191
+ if (state.used === state.body.length) {
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
+ }
199
+ this.state = state.padding === 0 ? { kind: "header" } : { kind: "data", remaining: state.padding };
200
+ }
201
+ continue;
202
+ }
101
203
  if (this.state.kind === "data") {
102
204
  const take = Math.min(this.state.remaining, chunk.length - offset);
103
205
  offset += take;
@@ -119,7 +221,15 @@ class TarMetadataMeter extends Transform {
119
221
  }
120
222
  _transform(chunk, _encoding, callback) {
121
223
  try {
122
- 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
+ }
123
233
  callback(null, chunk);
124
234
  }
125
235
  catch (error) {
@@ -127,8 +237,14 @@ class TarMetadataMeter extends Transform {
127
237
  }
128
238
  }
129
239
  _flush(callback) {
130
- if (this.state.kind === "header" && this.blockLength === 0)
240
+ if (this.pendingPax)
241
+ callback(this.invalid("dangling PAX metadata"));
242
+ else if (this.pendingGnu.size)
243
+ callback(this.invalid("dangling GNU metadata"));
244
+ else if (this.state.kind === "eof")
131
245
  callback();
246
+ else if (this.state.kind === "header" && this.blockLength === 0)
247
+ callback(this.invalid("missing two-block TAR EOF"));
132
248
  else
133
249
  callback(this.invalid(this.state.kind === "header" ? "truncated TAR header" : "truncated TAR entry"));
134
250
  }
@@ -146,7 +262,7 @@ async function isGzip(filePath) {
146
262
  }
147
263
  export async function preflightTarMetadata(params) {
148
264
  const input = fs.createReadStream(params.archivePath);
149
- const meter = new TarMetadataMeter(params.maxMetaEntryBytes);
265
+ const meter = new TarMetadataMeter(params.limits, params.onMember);
150
266
  const sink = new Writable({ write(_chunk, _encoding, callback) { callback(); } });
151
267
  if (await isGzip(params.archivePath)) {
152
268
  await pipeline(input, createGunzip(), meter, sink, { signal: params.signal });
@@ -0,0 +1,8 @@
1
+ export type LocalPax = {
2
+ path?: string;
3
+ linkpath?: string;
4
+ size?: number;
5
+ };
6
+ export declare function parseLocalPax(body: Buffer): LocalPax;
7
+ export declare function paxMemberSize(pax: LocalPax, type: number, rawSize: number, header: Buffer): number;
8
+ //# sourceMappingURL=archive-tar-pax.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-tar-pax.d.ts","sourceRoot":"","sources":["../src/archive-tar-pax.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,QAAQ,GAAG;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AAqB3E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,CAwCpD;AAQD,wBAAgB,aAAa,CAAC,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAclG"}
@@ -0,0 +1,100 @@
1
+ import { ArchiveFormatError } from "./archive-errors.js";
2
+ function invalid() {
3
+ throw new ArchiveFormatError("invalid TAR header: unsupported or malformed PAX metadata");
4
+ }
5
+ function decimal(bytes) {
6
+ const text = bytes.toString("latin1");
7
+ if (!/^(0|[1-9][0-9]*)$/.test(text))
8
+ invalid();
9
+ const value = Number(text);
10
+ if (!Number.isSafeInteger(value))
11
+ invalid();
12
+ return value;
13
+ }
14
+ function ascii(bytes) {
15
+ if (bytes.length === 0 || bytes.some((byte) => byte < 0x20 || byte > 0x7e))
16
+ invalid();
17
+ return bytes.toString("ascii");
18
+ }
19
+ // Keep this grammar aligned with native/src/tar_pax.rs. Downstream parsers
20
+ // disagree on duplicates, coercions and UTF-8 chunk boundaries.
21
+ export function parseLocalPax(body) {
22
+ if (body.length === 0)
23
+ invalid();
24
+ const result = {};
25
+ const keys = new Set();
26
+ let offset = 0;
27
+ while (offset < body.length) {
28
+ const space = body.indexOf(0x20, offset);
29
+ if (space < 0)
30
+ invalid();
31
+ const length = decimal(body.subarray(offset, space));
32
+ if (length > body.length - offset || length <= space - offset + 3)
33
+ invalid();
34
+ const end = offset + length;
35
+ if (body[end - 1] !== 0x0a)
36
+ invalid();
37
+ const record = body.subarray(space + 1, end - 1);
38
+ if (record.includes(0x0a))
39
+ invalid();
40
+ const equals = record.indexOf(0x3d);
41
+ if (equals <= 0)
42
+ invalid();
43
+ const key = ascii(record.subarray(0, equals));
44
+ if (!/^[A-Za-z0-9_.-]+$/.test(key) || keys.has(key))
45
+ invalid();
46
+ keys.add(key);
47
+ const value = record.subarray(equals + 1);
48
+ if (/^(LIBARCHIVE|SCHILY)\.xattr\..+$/.test(key)) {
49
+ // Inert bytes, never restored. Newlines would corrupt Rust's subsequent
50
+ // record lookup; NUL and non-UTF8 bytes in these values are harmless.
51
+ }
52
+ else if (key === "path" || key === "linkpath") {
53
+ result[key] = ascii(value);
54
+ }
55
+ else if (key === "size") {
56
+ result.size = decimal(value);
57
+ }
58
+ else if (key === "uid" || key === "gid") {
59
+ decimal(value);
60
+ }
61
+ else if (key === "uname" || key === "gname") {
62
+ ascii(value);
63
+ }
64
+ else if (key === "mtime" || key === "atime" || key === "ctime") {
65
+ const text = ascii(value);
66
+ if (!/^-?(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(text) || Math.abs(Number(text)) > 8_640_000_000_000)
67
+ invalid();
68
+ }
69
+ else {
70
+ invalid();
71
+ }
72
+ offset = end;
73
+ }
74
+ return result;
75
+ }
76
+ function rawText(field) {
77
+ const zero = field.indexOf(0);
78
+ const value = field.subarray(0, zero < 0 ? field.length : zero);
79
+ return value.length === 0 ? "" : ascii(value);
80
+ }
81
+ export function paxMemberSize(pax, type, rawSize, header) {
82
+ if (![0, 0x30, 0x31, 0x32, 0x35, 0x37].includes(type))
83
+ invalid();
84
+ const rawName = rawText(header.subarray(0, 100));
85
+ const rawLink = rawText(header.subarray(157, 257));
86
+ if (header.subarray(257, 265).equals(Buffer.from("ustar\0" + "00"))) {
87
+ rawText(header.subarray(345, 500));
88
+ }
89
+ const isLink = type === 0x31 || type === 0x32;
90
+ if (isLink !== (rawLink.length > 0))
91
+ invalid();
92
+ const size = pax.size ?? rawSize;
93
+ if ([0x31, 0x32, 0x35].includes(type) && (rawSize !== 0 || size !== 0))
94
+ invalid();
95
+ if (type !== 0x35 && (pax.path?.endsWith("/") || pax.path?.endsWith("\\") || rawName.endsWith("\\")))
96
+ invalid();
97
+ if (pax.linkpath !== undefined && !isLink)
98
+ invalid();
99
+ return size;
100
+ }
@@ -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;
@@ -25,8 +26,10 @@ export type TarModule = {
25
26
  preservePaths: false;
26
27
  noChmod: true;
27
28
  preserveOwner: false;
29
+ noMtime: true;
28
30
  strict: true;
29
31
  maxMetaEntrySize: number;
32
+ maxDecompressionRatio: number;
30
33
  filter?(this: TarParser, entryPath: string, entry: unknown): boolean;
31
34
  onReadEntry(this: unknown, entry: unknown): void;
32
35
  }): TarParser;
@@ -34,6 +37,7 @@ export type TarModule = {
34
37
  file: string;
35
38
  strict: true;
36
39
  maxMetaEntrySize: number;
40
+ maxDecompressionRatio: number;
37
41
  onReadEntry(entry: AsyncIterable<unknown> & {
38
42
  resume(): void;
39
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,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;
@@ -0,0 +1,7 @@
1
+ import type { ExtractionDeadline } from "./archive-deadline.js";
2
+ import { type ResolvedArchiveExtractLimits } from "./archive-limits.js";
3
+ /** Admit physical records before a decoder can normalize or collapse names. */
4
+ export declare function admitZipBuffer(input: Uint8Array, limits: ResolvedArchiveExtractLimits): number;
5
+ /** The extraction input is already private and staged; read only bounded metadata. */
6
+ export declare function admitZipFile(archivePath: string, limits: ResolvedArchiveExtractLimits, deadline: ExtractionDeadline): Promise<number>;
7
+ //# sourceMappingURL=archive-zip-admission.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-zip-admission.d.ts","sourceRoot":"","sources":["../src/archive-zip-admission.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAA+C,KAAK,4BAA4B,EAAE,MAAM,qBAAqB,CAAC;AAYrH,+EAA+E;AAC/E,wBAAgB,cAAc,CAAC,KAAK,EAAE,UAAU,EAAE,MAAM,EAAE,4BAA4B,GAAG,MAAM,CAU9F;AAED,sFAAsF;AACtF,wBAAsB,YAAY,CAAC,WAAW,EAAE,MAAM,EAAE,MAAM,EAAE,4BAA4B,EAAE,QAAQ,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,CA+B3I"}
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { ARCHIVE_LIMIT_ERROR_CODE, ArchiveLimitError } from "./archive-limits.js";
4
+ import { openRootFile } from "./root-file.js";
5
+ import { scanZipDirectory } from "./archive-zip-directory.js";
6
+ import { zipFormat } from "./archive-zip-names.js";
7
+ function checkSize(size, limits) {
8
+ if (size > limits.maxArchiveBytes) {
9
+ throw new ArchiveLimitError(ARCHIVE_LIMIT_ERROR_CODE.ARCHIVE_SIZE_EXCEEDS_LIMIT);
10
+ }
11
+ if (!Number.isSafeInteger(size) || size < 0)
12
+ zipFormat("invalid archive size");
13
+ }
14
+ /** Admit physical records before a decoder can normalize or collapse names. */
15
+ export function admitZipBuffer(input, limits) {
16
+ checkSize(input.byteLength, limits);
17
+ const buffer = Buffer.from(input.buffer, input.byteOffset, input.byteLength);
18
+ const scan = scanZipDirectory(buffer.length, limits);
19
+ let step = scan.next();
20
+ while (!step.done) {
21
+ const { offset, length } = step.value;
22
+ step = scan.next(buffer.subarray(offset, offset + length));
23
+ }
24
+ return step.value;
25
+ }
26
+ /** The extraction input is already private and staged; read only bounded metadata. */
27
+ export async function admitZipFile(archivePath, limits, deadline) {
28
+ deadline.check();
29
+ const opened = await openRootFile({
30
+ absolutePath: archivePath, rootPath: path.dirname(archivePath),
31
+ boundaryLabel: "staged ZIP archive", rejectSymlinks: true, rejectHardlinks: true,
32
+ });
33
+ if (!opened.ok)
34
+ throw opened.error ?? new Error("cannot open staged ZIP archive");
35
+ try {
36
+ checkSize(opened.stat.size, limits);
37
+ const scan = scanZipDirectory(opened.stat.size, limits);
38
+ let step = scan.next();
39
+ while (!step.done) {
40
+ const { offset, length } = step.value;
41
+ const bytes = Buffer.alloc(length);
42
+ let received = 0;
43
+ while (received < length) {
44
+ deadline.check();
45
+ const count = await new Promise((resolve, reject) => {
46
+ fs.read(opened.fd, bytes, received, length - received, offset + received, (error, count) => error ? reject(error) : resolve(count));
47
+ });
48
+ deadline.check();
49
+ if (!count)
50
+ zipFormat("truncated staged record");
51
+ received += count;
52
+ }
53
+ step = scan.next(bytes);
54
+ }
55
+ return step.value;
56
+ }
57
+ finally {
58
+ fs.closeSync(opened.fd);
59
+ }
60
+ }
@@ -0,0 +1,2 @@
1
+ export declare function readZipCentralDirectoryEntryCount(buffer: Buffer | Uint8Array): number | null;
2
+ //# sourceMappingURL=archive-zip-count.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-zip-count.d.ts","sourceRoot":"","sources":["../src/archive-zip-count.ts"],"names":[],"mappings":"AA4KA,wBAAgB,iCAAiC,CAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,GAAG,IAAI,CAU5F"}
@@ -0,0 +1,140 @@
1
+ // Conservative public count hint; strict admission must not rely on its fallback.
2
+ const ZIP_EOCD_SIGNATURE = 0x06054b50;
3
+ const ZIP64_EOCD_SIGNATURE = 0x06064b50;
4
+ const ZIP64_EOCD_LOCATOR_SIGNATURE = 0x07064b50;
5
+ const ZIP_EOCD_MIN_BYTES = 22;
6
+ const ZIP_EOCD_MAX_COMMENT_BYTES = 0xffff;
7
+ const ZIP64_ENTRY_COUNT_SENTINEL = 0xffff;
8
+ const ZIP64_UINT32_SENTINEL = 0xffffffff;
9
+ const ZIP_CENTRAL_FILE_HEADER_SIGNATURE = 0x02014b50;
10
+ const ZIP_CENTRAL_FILE_HEADER_MIN_BYTES = 46;
11
+ const ZIP_CENTRAL_FILE_HEADER_NAME_LENGTH_OFFSET = 28;
12
+ const ZIP_CENTRAL_FILE_HEADER_EXTRA_LENGTH_OFFSET = 30;
13
+ const ZIP_CENTRAL_FILE_HEADER_COMMENT_LENGTH_OFFSET = 32;
14
+ const ZIP_EOCD_TOTAL_ENTRIES_OFFSET = 10;
15
+ const ZIP_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET = 12;
16
+ const ZIP_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET = 16;
17
+ const ZIP_EOCD_COMMENT_LENGTH_OFFSET = 20;
18
+ const ZIP64_EOCD_LOCATOR_BYTES = 20;
19
+ const ZIP64_EOCD_OFFSET_OFFSET = 8;
20
+ const ZIP64_EOCD_TOTAL_ENTRIES_OFFSET = 32;
21
+ const ZIP64_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET = 40;
22
+ const ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET = 48;
23
+ function asBufferView(buffer) {
24
+ if (Buffer.isBuffer(buffer)) {
25
+ return buffer;
26
+ }
27
+ return Buffer.from(buffer.buffer, buffer.byteOffset, buffer.byteLength);
28
+ }
29
+ function readSafeUInt64LE(buffer, offset) {
30
+ const value = buffer.readBigUInt64LE(offset);
31
+ if (value > BigInt(Number.MAX_SAFE_INTEGER)) {
32
+ return Number.MAX_SAFE_INTEGER;
33
+ }
34
+ return Number(value);
35
+ }
36
+ function findZipEndOfCentralDirectory(buffer) {
37
+ if (buffer.byteLength < ZIP_EOCD_MIN_BYTES) {
38
+ return -1;
39
+ }
40
+ const minOffset = Math.max(0, buffer.byteLength - ZIP_EOCD_MIN_BYTES - ZIP_EOCD_MAX_COMMENT_BYTES);
41
+ for (let offset = buffer.byteLength - ZIP_EOCD_MIN_BYTES; offset >= minOffset; offset -= 1) {
42
+ if (buffer.readUInt32LE(offset) !== ZIP_EOCD_SIGNATURE) {
43
+ continue;
44
+ }
45
+ const commentLength = buffer.readUInt16LE(offset + ZIP_EOCD_COMMENT_LENGTH_OFFSET);
46
+ if (offset + ZIP_EOCD_MIN_BYTES + commentLength === buffer.byteLength) {
47
+ return offset;
48
+ }
49
+ }
50
+ return -1;
51
+ }
52
+ function readZip64CentralDirectoryInfo(buffer, eocdOffset) {
53
+ const locatorOffset = eocdOffset - ZIP64_EOCD_LOCATOR_BYTES;
54
+ if (locatorOffset < 0 || buffer.readUInt32LE(locatorOffset) !== ZIP64_EOCD_LOCATOR_SIGNATURE) {
55
+ return null;
56
+ }
57
+ const zip64EocdOffset = readSafeUInt64LE(buffer, locatorOffset + ZIP64_EOCD_OFFSET_OFFSET);
58
+ if (zip64EocdOffset < 0 ||
59
+ zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET + 8 > buffer.byteLength ||
60
+ buffer.readUInt32LE(zip64EocdOffset) !== ZIP64_EOCD_SIGNATURE) {
61
+ return null;
62
+ }
63
+ return {
64
+ declaredEntryCount: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_TOTAL_ENTRIES_OFFSET),
65
+ centralDirectorySize: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET),
66
+ centralDirectoryOffset: readSafeUInt64LE(buffer, zip64EocdOffset + ZIP64_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET),
67
+ endOfCentralDirectoryOffset: eocdOffset,
68
+ };
69
+ }
70
+ function readZipCentralDirectoryInfo(buffer) {
71
+ const eocdOffset = findZipEndOfCentralDirectory(buffer);
72
+ if (eocdOffset < 0) {
73
+ return null;
74
+ }
75
+ const declaredEntryCount = buffer.readUInt16LE(eocdOffset + ZIP_EOCD_TOTAL_ENTRIES_OFFSET);
76
+ const centralDirectorySize = buffer.readUInt32LE(eocdOffset + ZIP_EOCD_CENTRAL_DIRECTORY_SIZE_OFFSET);
77
+ const centralDirectoryOffset = buffer.readUInt32LE(eocdOffset + ZIP_EOCD_CENTRAL_DIRECTORY_OFFSET_OFFSET);
78
+ const usesZip64 = declaredEntryCount === ZIP64_ENTRY_COUNT_SENTINEL ||
79
+ centralDirectorySize === ZIP64_UINT32_SENTINEL ||
80
+ centralDirectoryOffset === ZIP64_UINT32_SENTINEL;
81
+ if (usesZip64) {
82
+ return (readZip64CentralDirectoryInfo(buffer, eocdOffset) ?? {
83
+ declaredEntryCount,
84
+ centralDirectoryOffset,
85
+ centralDirectorySize,
86
+ endOfCentralDirectoryOffset: eocdOffset,
87
+ });
88
+ }
89
+ return {
90
+ declaredEntryCount,
91
+ centralDirectoryOffset,
92
+ centralDirectorySize,
93
+ endOfCentralDirectoryOffset: eocdOffset,
94
+ };
95
+ }
96
+ function countZipCentralDirectoryHeaders(buffer, info) {
97
+ const start = info.centralDirectoryOffset;
98
+ const declaredEnd = start + info.centralDirectorySize;
99
+ const scanEnd = info.endOfCentralDirectoryOffset;
100
+ if (!Number.isSafeInteger(start) ||
101
+ !Number.isSafeInteger(declaredEnd) ||
102
+ !Number.isSafeInteger(scanEnd) ||
103
+ start < 0 ||
104
+ declaredEnd < start ||
105
+ scanEnd < start ||
106
+ scanEnd > buffer.byteLength) {
107
+ return null;
108
+ }
109
+ let offset = start;
110
+ let count = 0;
111
+ while (offset < scanEnd) {
112
+ if (scanEnd - offset < ZIP_CENTRAL_FILE_HEADER_MIN_BYTES) {
113
+ break;
114
+ }
115
+ if (buffer.readUInt32LE(offset) !== ZIP_CENTRAL_FILE_HEADER_SIGNATURE) {
116
+ break;
117
+ }
118
+ const nameLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_NAME_LENGTH_OFFSET);
119
+ const extraLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_EXTRA_LENGTH_OFFSET);
120
+ const commentLength = buffer.readUInt16LE(offset + ZIP_CENTRAL_FILE_HEADER_COMMENT_LENGTH_OFFSET);
121
+ const nextOffset = offset + ZIP_CENTRAL_FILE_HEADER_MIN_BYTES + nameLength + extraLength + commentLength;
122
+ if (nextOffset <= offset || nextOffset > scanEnd) {
123
+ return null;
124
+ }
125
+ count += 1;
126
+ offset = nextOffset;
127
+ }
128
+ return count > 0 || info.declaredEntryCount === 0 ? count : null;
129
+ }
130
+ export function readZipCentralDirectoryEntryCount(buffer) {
131
+ const view = asBufferView(buffer);
132
+ const info = readZipCentralDirectoryInfo(view);
133
+ if (!info) {
134
+ return null;
135
+ }
136
+ const countedEntryCount = countZipCentralDirectoryHeaders(view, info);
137
+ return countedEntryCount === null
138
+ ? info.declaredEntryCount
139
+ : Math.max(info.declaredEntryCount, countedEntryCount);
140
+ }