@@ -5,6 +5,7 @@ import { readStringValue } from "../shared/string-coerce.js";
|
5 | 5 | import { isRecord, resolveUserPath } from "../utils.js"; |
6 | 6 | |
7 | 7 | const WINDOWS_ABSOLUTE_ARCHIVE_PATH_RE = /^[A-Za-z]:[\\/]/; |
| 8 | +const MAX_MANIFEST_BYTES = 1024 * 1024; |
8 | 9 | |
9 | 10 | type BackupManifestAsset = { |
10 | 11 | kind: string; |
@@ -193,7 +194,7 @@ async function extractManifest(params: {
|
193 | 194 | archivePath: string; |
194 | 195 | manifestEntryPath: string; |
195 | 196 | }): Promise<string> { |
196 | | -let manifestContentPromise: Promise<string> | undefined; |
| 197 | +let manifestContentPromise: Promise<{ content?: string; error?: Error }> | undefined; |
197 | 198 | await tar.t({ |
198 | 199 | file: params.archivePath, |
199 | 200 | gzip: true, |
@@ -203,14 +204,44 @@ async function extractManifest(params: {
|
203 | 204 | return; |
204 | 205 | } |
205 | 206 | |
206 | | -manifestContentPromise = new Promise<string>((resolve, reject) => { |
| 207 | +manifestContentPromise = new Promise<{ content?: string; error?: Error }>((resolve) => { |
207 | 208 | const chunks: Buffer[] = []; |
| 209 | +let totalBytes = 0; |
| 210 | +let exceededLimit = false; |
| 211 | +let settled = false; |
| 212 | +const settle = (result: { content?: string; error?: Error }) => { |
| 213 | +if (settled) { |
| 214 | +return; |
| 215 | +} |
| 216 | +settled = true; |
| 217 | +resolve(result); |
| 218 | +}; |
208 | 219 | entry.on("data", (chunk: Buffer | string) => { |
209 | | -chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); |
| 220 | +if (exceededLimit) { |
| 221 | +return; |
| 222 | +} |
| 223 | +const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk); |
| 224 | +totalBytes += buffer.byteLength; |
| 225 | +if (totalBytes > MAX_MANIFEST_BYTES) { |
| 226 | +exceededLimit = true; |
| 227 | +chunks.length = 0; |
| 228 | +return; |
| 229 | +} |
| 230 | +chunks.push(buffer); |
| 231 | +}); |
| 232 | +entry.on("error", (error) => { |
| 233 | +settle({ |
| 234 | +error: error instanceof Error ? error : new Error(String(error)), |
| 235 | +}); |
210 | 236 | }); |
211 | | -entry.on("error", reject); |
212 | 237 | entry.on("end", () => { |
213 | | -resolve(Buffer.concat(chunks).toString("utf8")); |
| 238 | +if (exceededLimit) { |
| 239 | +settle({ |
| 240 | +error: new Error(`Backup manifest exceeds ${MAX_MANIFEST_BYTES} byte limit.`), |
| 241 | +}); |
| 242 | +return; |
| 243 | +} |
| 244 | +settle({ content: Buffer.concat(chunks, totalBytes).toString("utf8") }); |
214 | 245 | }); |
215 | 246 | }); |
216 | 247 | }, |
@@ -219,7 +250,11 @@ async function extractManifest(params: {
|
219 | 250 | if (!manifestContentPromise) { |
220 | 251 | throw new Error(`Archive is missing manifest entry: ${params.manifestEntryPath}`); |
221 | 252 | } |
222 | | -return await manifestContentPromise; |
| 253 | +const result = await manifestContentPromise; |
| 254 | +if (result.error) { |
| 255 | +throw result.error; |
| 256 | +} |
| 257 | +return result.content ?? ""; |
223 | 258 | } |
224 | 259 | |
225 | 260 | function isRootManifestEntry(entryPath: string): boolean { |
|