FileMiddleware.js 31 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052
  1. /*
  2. MIT License http://www.opensource.org/licenses/mit-license.php
  3. */
  4. "use strict";
  5. const { constants } = require("buffer");
  6. const { pipeline } = require("stream");
  7. const {
  8. // eslint-disable-next-line n/no-unsupported-features/node-builtins
  9. brotliDecompress,
  10. constants: zConstants,
  11. // eslint-disable-next-line n/no-unsupported-features/node-builtins
  12. createBrotliCompress,
  13. // eslint-disable-next-line n/no-unsupported-features/node-builtins
  14. createBrotliDecompress,
  15. createGunzip,
  16. createGzip,
  17. // zstd is only available on Node.js >= 22.15; guarded at use sites
  18. // eslint-disable-next-line n/no-unsupported-features/node-builtins
  19. createZstdCompress,
  20. // eslint-disable-next-line n/no-unsupported-features/node-builtins
  21. createZstdDecompress,
  22. gunzip,
  23. // eslint-disable-next-line n/no-unsupported-features/node-builtins
  24. zstdDecompress
  25. } = require("zlib");
  26. const { DEFAULTS } = require("../config/defaults");
  27. const createHash = require("../util/createHash");
  28. const { dirname, join, mkdirp } = require("../util/fs");
  29. const memoize = require("../util/memoize");
  30. const SerializerMiddleware = require("./SerializerMiddleware");
  31. /** @import { HashFunction } from "../util/Hash" */
  32. /** @import { IStats, IntermediateFileSystem } from "../util/fs" */
  33. /** @import { BufferSerializableType } from "./types" */
  34. /*
  35. Format:
  36. File -> Header Section*
  37. Version -> u32
  38. AmountOfSections -> u32
  39. SectionSize -> i32 (if less than zero represents lazy value)
  40. Header -> Version AmountOfSections SectionSize*
  41. Buffer -> n bytes
  42. Section -> Buffer
  43. */
  44. // "wpc" + 1 in little-endian
  45. const VERSION = 0x01637077;
  46. const WRITE_LIMIT_TOTAL = 0x7fff0000;
  47. const WRITE_LIMIT_CHUNK = 511 * 1024 * 1024;
  48. // headers and pointer sections are tiny; anything above this is a corrupt file
  49. const MAX_HEADER_OR_POINTER_SIZE = 256 * 1024 * 1024;
  50. /**
  51. * Returns hash.
  52. * @param {Buffer[]} buffers buffers
  53. * @param {HashFunction} hashFunction hash function to use
  54. * @returns {string} hash
  55. */
  56. const hashForName = (buffers, hashFunction) => {
  57. const hash = createHash(hashFunction);
  58. for (const buf of buffers) hash.update(buf);
  59. return hash.digest("hex");
  60. };
  61. const COMPRESSION_CHUNK_SIZE = 100 * 1024 * 1024;
  62. const DECOMPRESSION_CHUNK_SIZE = 100 * 1024 * 1024;
  63. /** @type {(buffer: Buffer, value: number, offset: number) => void} */
  64. const writeUInt64LE = Buffer.prototype.writeBigUInt64LE
  65. ? (buf, value, offset) => {
  66. buf.writeBigUInt64LE(BigInt(value), offset);
  67. }
  68. : (buf, value, offset) => {
  69. const low = value % 0x100000000;
  70. const high = (value - low) / 0x100000000;
  71. buf.writeUInt32LE(low, offset);
  72. buf.writeUInt32LE(high, offset + 4);
  73. };
  74. /** @type {(buffer: Buffer, offset: number) => void} */
  75. const readUInt64LE = Buffer.prototype.readBigUInt64LE
  76. ? (buf, offset) => Number(buf.readBigUInt64LE(offset))
  77. : (buf, offset) => {
  78. const low = buf.readUInt32LE(offset);
  79. const high = buf.readUInt32LE(offset + 4);
  80. return high * 0x100000000 + low;
  81. };
  82. /** @typedef {Promise<void | void[]>} BackgroundJob */
  83. /**
  84. * Defines the serialize result type used by this module.
  85. * @typedef {object} SerializeResult
  86. * @property {string | false} name
  87. * @property {number} size
  88. * @property {BackgroundJob=} backgroundJob
  89. */
  90. /** @typedef {{ name: string, size: number }} LazyOptions */
  91. /**
  92. * Defines the lazy function type used by this module.
  93. * @typedef {import("./SerializerMiddleware").LazyFunction<BufferSerializableType[], Buffer, FileMiddleware, LazyOptions>} LazyFunction
  94. */
  95. /**
  96. * Serializes this instance into the provided serializer context.
  97. * @param {FileMiddleware} middleware this
  98. * @param {(BufferSerializableType | LazyFunction)[]} data data to be serialized
  99. * @param {string | boolean} name file base name
  100. * @param {(name: string | false, buffers: Buffer[], size: number) => Promise<void>} writeFile writes a file
  101. * @param {HashFunction=} hashFunction hash function to use
  102. * @param {Set<string>=} retainedNames collects names of files that stay referenced without being rewritten
  103. * @returns {Promise<SerializeResult>} resulting file pointer and promise
  104. */
  105. const serialize = async (
  106. middleware,
  107. data,
  108. name,
  109. writeFile,
  110. hashFunction = DEFAULTS.HASH_FUNCTION,
  111. retainedNames = undefined
  112. ) => {
  113. /** @type {(Buffer[] | Buffer | Promise<SerializeResult>)[]} */
  114. const processedData = [];
  115. /** @type {WeakMap<SerializeResult, LazyFunction>} */
  116. const resultToLazy = new WeakMap();
  117. /** @type {Buffer[] | undefined} */
  118. let lastBuffers;
  119. for (const item of await data) {
  120. if (typeof item === "function") {
  121. if (!SerializerMiddleware.isLazy(item)) {
  122. throw new Error("Unexpected function");
  123. }
  124. if (!SerializerMiddleware.isLazy(item, middleware)) {
  125. throw new Error(
  126. "Unexpected lazy value with non-this target (can't pass through lazy values)"
  127. );
  128. }
  129. lastBuffers = undefined;
  130. const serializedInfo = SerializerMiddleware.getLazySerializedValue(item);
  131. if (serializedInfo) {
  132. if (typeof serializedInfo === "function") {
  133. throw new Error(
  134. "Unexpected lazy value with non-this target (can't pass through lazy values)"
  135. );
  136. } else {
  137. if (retainedNames !== undefined) {
  138. // pointer buffer layout: u64 size + utf-8 file name
  139. retainedNames.add(serializedInfo.toString("utf8", 8));
  140. }
  141. processedData.push(serializedInfo);
  142. }
  143. } else {
  144. const content = item();
  145. if (content) {
  146. const options = SerializerMiddleware.getLazyOptions(item);
  147. processedData.push(
  148. serialize(
  149. middleware,
  150. /** @type {BufferSerializableType[]} */
  151. (content),
  152. (options && options.name) || true,
  153. writeFile,
  154. hashFunction,
  155. retainedNames
  156. ).then((result) => {
  157. /** @type {LazyOptions} */
  158. (item.options).size = result.size;
  159. resultToLazy.set(result, item);
  160. return result;
  161. })
  162. );
  163. } else {
  164. throw new Error(
  165. "Unexpected falsy value returned by lazy value function"
  166. );
  167. }
  168. }
  169. } else if (item) {
  170. if (lastBuffers) {
  171. lastBuffers.push(item);
  172. } else {
  173. lastBuffers = [item];
  174. processedData.push(lastBuffers);
  175. }
  176. } else {
  177. throw new Error("Unexpected falsy value in items array");
  178. }
  179. }
  180. /** @type {BackgroundJob[]} */
  181. const backgroundJobs = [];
  182. const resolvedData = (await Promise.all(processedData)).map((item) => {
  183. if (Array.isArray(item) || Buffer.isBuffer(item)) return item;
  184. backgroundJobs.push(
  185. /** @type {BackgroundJob} */
  186. (item.backgroundJob)
  187. );
  188. // create pointer buffer from size and name
  189. const name = /** @type {string} */ (item.name);
  190. const nameBuffer = Buffer.from(name);
  191. const buf = Buffer.allocUnsafe(8 + nameBuffer.length);
  192. writeUInt64LE(buf, item.size, 0);
  193. nameBuffer.copy(buf, 8, 0);
  194. const lazy =
  195. /** @type {LazyFunction} */
  196. (resultToLazy.get(item));
  197. SerializerMiddleware.setLazySerializedValue(lazy, buf);
  198. return buf;
  199. });
  200. /** @type {number[]} */
  201. const lengths = [];
  202. for (const item of resolvedData) {
  203. if (Array.isArray(item)) {
  204. let l = 0;
  205. for (const b of item) l += b.length;
  206. while (l > 0x7fffffff) {
  207. lengths.push(0x7fffffff);
  208. l -= 0x7fffffff;
  209. }
  210. lengths.push(l);
  211. } else if (item) {
  212. lengths.push(-item.length);
  213. } else {
  214. throw new Error(`Unexpected falsy value in resolved data ${item}`);
  215. }
  216. }
  217. const header = Buffer.allocUnsafe(8 + lengths.length * 4);
  218. header.writeUInt32LE(VERSION, 0);
  219. header.writeUInt32LE(lengths.length, 4);
  220. for (let i = 0; i < lengths.length; i++) {
  221. header.writeInt32LE(lengths[i], 8 + i * 4);
  222. }
  223. /** @type {Buffer[]} */
  224. const buf = [header];
  225. for (const item of resolvedData) {
  226. if (Array.isArray(item)) {
  227. for (const b of item) buf.push(b);
  228. } else if (item) {
  229. buf.push(item);
  230. }
  231. }
  232. if (name === true) {
  233. name = hashForName(buf, hashFunction);
  234. }
  235. let size = 0;
  236. for (const b of buf) size += b.length;
  237. backgroundJobs.push(writeFile(name, buf, size));
  238. return {
  239. size,
  240. name,
  241. backgroundJob:
  242. backgroundJobs.length === 1
  243. ? backgroundJobs[0]
  244. : /** @type {BackgroundJob} */ (Promise.all(backgroundJobs))
  245. };
  246. };
  247. /**
  248. * Restores this instance from the provided deserializer context.
  249. * @param {FileMiddleware} middleware this
  250. * @param {string | false} name filename
  251. * @param {(name: string | false) => Promise<Buffer[]>} readFile read content of a file
  252. * @returns {Promise<BufferSerializableType[]>} deserialized data
  253. */
  254. const deserialize = async (middleware, name, readFile) => {
  255. const contents = await readFile(name);
  256. if (contents.length === 0) throw new Error(`Empty file ${name}`);
  257. let contentsIndex = 0;
  258. let contentItem = contents[0];
  259. let contentItemLength = contentItem.length;
  260. let contentPosition = 0;
  261. if (contentItemLength === 0) throw new Error(`Empty file ${name}`);
  262. const nextContent = () => {
  263. contentsIndex++;
  264. contentItem = contents[contentsIndex];
  265. contentItemLength = contentItem.length;
  266. contentPosition = 0;
  267. };
  268. /**
  269. * Processes the provided n.
  270. * @param {number} n number of bytes to ensure
  271. */
  272. const ensureData = (n) => {
  273. if (contentPosition === contentItemLength) {
  274. nextContent();
  275. }
  276. while (contentItemLength - contentPosition < n) {
  277. const remaining = contentItem.subarray(contentPosition);
  278. let lengthFromNext = n - remaining.length;
  279. /** @type {Buffer[]} */
  280. const buffers = [remaining];
  281. for (let i = contentsIndex + 1; i < contents.length; i++) {
  282. const l = contents[i].length;
  283. if (l > lengthFromNext) {
  284. buffers.push(contents[i].subarray(0, lengthFromNext));
  285. contents[i] = contents[i].subarray(lengthFromNext);
  286. lengthFromNext = 0;
  287. break;
  288. } else {
  289. buffers.push(contents[i]);
  290. contentsIndex = i;
  291. lengthFromNext -= l;
  292. }
  293. }
  294. if (lengthFromNext > 0) throw new Error("Unexpected end of data");
  295. contentItem = Buffer.concat(buffers, n);
  296. contentItemLength = n;
  297. contentPosition = 0;
  298. }
  299. };
  300. /**
  301. * Returns value value.
  302. * @returns {number} value value
  303. */
  304. const readUInt32LE = () => {
  305. ensureData(4);
  306. const value = contentItem.readUInt32LE(contentPosition);
  307. contentPosition += 4;
  308. return value;
  309. };
  310. /**
  311. * Returns value value.
  312. * @returns {number} value value
  313. */
  314. const readInt32LE = () => {
  315. ensureData(4);
  316. const value = contentItem.readInt32LE(contentPosition);
  317. contentPosition += 4;
  318. return value;
  319. };
  320. /**
  321. * Returns buffer.
  322. * @param {number} l length
  323. * @returns {Buffer} buffer
  324. */
  325. const readSlice = (l) => {
  326. ensureData(l);
  327. if (contentPosition === 0 && contentItemLength === l) {
  328. const result = contentItem;
  329. if (contentsIndex + 1 < contents.length) {
  330. nextContent();
  331. } else {
  332. contentPosition = l;
  333. }
  334. return result;
  335. }
  336. const result = contentItem.subarray(contentPosition, contentPosition + l);
  337. contentPosition += l;
  338. // we clone the buffer here to allow the original content to be garbage collected
  339. return l * 2 < contentItem.buffer.byteLength ? Buffer.from(result) : result;
  340. };
  341. const version = readUInt32LE();
  342. if (version !== VERSION) {
  343. throw new Error("Invalid file version");
  344. }
  345. const sectionCount = readUInt32LE();
  346. /** @type {number[]} */
  347. const lengths = [];
  348. let lastLengthPositive = false;
  349. for (let i = 0; i < sectionCount; i++) {
  350. const value = readInt32LE();
  351. const valuePositive = value >= 0;
  352. if (lastLengthPositive && valuePositive) {
  353. lengths[lengths.length - 1] += value;
  354. } else {
  355. lengths.push(value);
  356. lastLengthPositive = valuePositive;
  357. }
  358. }
  359. /** @type {BufferSerializableType[]} */
  360. const result = [];
  361. for (let length of lengths) {
  362. if (length < 0) {
  363. const slice = readSlice(-length);
  364. const size = Number(readUInt64LE(slice, 0));
  365. const nameBuffer = slice.subarray(8);
  366. const name = nameBuffer.toString();
  367. const lazy =
  368. /** @type {LazyFunction} */
  369. (
  370. SerializerMiddleware.createLazy(
  371. memoize(() => deserialize(middleware, name, readFile)),
  372. middleware,
  373. { name, size },
  374. slice
  375. )
  376. );
  377. result.push(lazy);
  378. } else {
  379. // A section may start exactly at a content-buffer boundary; advance
  380. // first, then read from the fresh buffer (don't fall through to the
  381. // `while` below, which would skip it).
  382. if (contentPosition === contentItemLength) {
  383. nextContent();
  384. }
  385. if (contentPosition !== 0) {
  386. if (length <= contentItemLength - contentPosition) {
  387. result.push(
  388. Buffer.from(
  389. contentItem.buffer,
  390. contentItem.byteOffset + contentPosition,
  391. length
  392. )
  393. );
  394. contentPosition += length;
  395. length = 0;
  396. } else {
  397. const l = contentItemLength - contentPosition;
  398. result.push(
  399. Buffer.from(
  400. contentItem.buffer,
  401. contentItem.byteOffset + contentPosition,
  402. l
  403. )
  404. );
  405. length -= l;
  406. contentPosition = contentItemLength;
  407. }
  408. } else if (length >= contentItemLength) {
  409. result.push(contentItem);
  410. length -= contentItemLength;
  411. contentPosition = contentItemLength;
  412. } else {
  413. result.push(
  414. Buffer.from(contentItem.buffer, contentItem.byteOffset, length)
  415. );
  416. contentPosition += length;
  417. length = 0;
  418. }
  419. while (length > 0) {
  420. nextContent();
  421. if (length >= contentItemLength) {
  422. result.push(contentItem);
  423. length -= contentItemLength;
  424. contentPosition = contentItemLength;
  425. } else {
  426. result.push(
  427. Buffer.from(contentItem.buffer, contentItem.byteOffset, length)
  428. );
  429. contentPosition += length;
  430. length = 0;
  431. }
  432. }
  433. }
  434. }
  435. return result;
  436. };
  437. /** @typedef {BufferSerializableType[]} DeserializedType */
  438. /** @typedef {true} SerializedType */
  439. /**
  440. * `writtenFiles`/`retainedFiles` collect file names (without extension) during
  441. * `serialize`: files written in this run and files that stay referenced by
  442. * lazy pointers without being rewritten. Retained files may reference further
  443. * files on disk not listed here (nested lazy segments); use
  444. * `getReferencedFilenames` to walk them.
  445. * @typedef {{ filename: string, extension?: string, writtenFiles?: Set<string>, retainedFiles?: Set<string> }} Context
  446. */
  447. /**
  448. * Represents FileMiddleware.
  449. * @extends {SerializerMiddleware<DeserializedType, SerializedType, Context>}
  450. */
  451. class FileMiddleware extends SerializerMiddleware {
  452. /**
  453. * Creates an instance of FileMiddleware.
  454. * @param {IntermediateFileSystem} fs filesystem
  455. * @param {HashFunction} hashFunction hash function to use
  456. */
  457. constructor(fs, hashFunction = DEFAULTS.HASH_FUNCTION) {
  458. super();
  459. /** @type {IntermediateFileSystem} */
  460. this.fs = fs;
  461. /** @type {HashFunction} */
  462. this._hashFunction = hashFunction;
  463. }
  464. /**
  465. * Serializes this instance into the provided serializer context.
  466. * @param {DeserializedType} data data
  467. * @param {Context} context context object
  468. * @returns {SerializedType | Promise<SerializedType> | null} serialized data
  469. */
  470. serialize(data, context) {
  471. const { filename, extension = "", writtenFiles, retainedFiles } = context;
  472. return new Promise((resolve, reject) => {
  473. mkdirp(this.fs, dirname(this.fs, filename), (err) => {
  474. if (err) return reject(err);
  475. // It's important that we don't touch existing files during serialization
  476. // because serialize may read existing files (when deserializing)
  477. /** @type {Set<string>} */
  478. const allWrittenFiles = new Set();
  479. /**
  480. * Processes the provided name.
  481. * @param {string | false} name name
  482. * @param {Buffer[]} content content
  483. * @param {number} size size
  484. * @returns {Promise<void>}
  485. */
  486. const writeFile = async (name, content, size) => {
  487. const file = name
  488. ? join(this.fs, filename, `../${name}${extension}`)
  489. : filename;
  490. await new Promise(
  491. /**
  492. * Handles the callback logic for this hook.
  493. * @param {(value?: undefined) => void} resolve resolve
  494. * @param {(reason?: Error | null) => void} reject reject
  495. */
  496. (resolve, reject) => {
  497. let stream = this.fs.createWriteStream(`${file}_`);
  498. /** @type {undefined | import("zlib").Gzip | import("zlib").BrotliCompress | import("zlib").ZstdCompress} */
  499. let compression;
  500. if (file.endsWith(".gz")) {
  501. compression = createGzip({
  502. chunkSize: COMPRESSION_CHUNK_SIZE,
  503. level: zConstants.Z_BEST_SPEED
  504. });
  505. } else if (file.endsWith(".br")) {
  506. compression = createBrotliCompress({
  507. chunkSize: COMPRESSION_CHUNK_SIZE,
  508. params: {
  509. [zConstants.BROTLI_PARAM_MODE]: zConstants.BROTLI_MODE_TEXT,
  510. [zConstants.BROTLI_PARAM_QUALITY]: 2,
  511. [zConstants.BROTLI_PARAM_DISABLE_LITERAL_CONTEXT_MODELING]: true,
  512. [zConstants.BROTLI_PARAM_SIZE_HINT]: size
  513. }
  514. });
  515. } else if (file.endsWith(".zst")) {
  516. // eslint-disable-next-line n/no-unsupported-features/node-builtins
  517. const levelParam = zConstants.ZSTD_c_compressionLevel;
  518. // default level 3; some runtimes (e.g. Deno) don't expose ZSTD_CLEVEL_DEFAULT
  519. // eslint-disable-next-line n/no-unsupported-features/node-builtins
  520. const defaultLevel = zConstants.ZSTD_CLEVEL_DEFAULT;
  521. const level = defaultLevel === undefined ? 3 : defaultLevel;
  522. compression = createZstdCompress({
  523. chunkSize: COMPRESSION_CHUNK_SIZE,
  524. params: { [levelParam]: level }
  525. });
  526. }
  527. if (compression) {
  528. pipeline(compression, stream, reject);
  529. stream = compression;
  530. stream.on("finish", () => resolve());
  531. } else {
  532. stream.on("error", (err) => reject(err));
  533. stream.on("finish", () => resolve());
  534. }
  535. // split into chunks for WRITE_LIMIT_CHUNK size
  536. /** @type {Buffer[]} */
  537. const chunks = [];
  538. for (const b of content) {
  539. if (b.length < WRITE_LIMIT_CHUNK) {
  540. chunks.push(b);
  541. } else {
  542. for (let i = 0; i < b.length; i += WRITE_LIMIT_CHUNK) {
  543. chunks.push(b.subarray(i, i + WRITE_LIMIT_CHUNK));
  544. }
  545. }
  546. }
  547. const len = chunks.length;
  548. let i = 0;
  549. /**
  550. * Processes the provided err.
  551. * @param {(Error | null)=} err err
  552. */
  553. const batchWrite = (err) => {
  554. // will be handled in "on" error handler
  555. if (err) return;
  556. if (i === len) {
  557. stream.end();
  558. return;
  559. }
  560. // queue up a batch of chunks up to the write limit
  561. // end is exclusive
  562. let end = i;
  563. let sum = chunks[end++].length;
  564. while (end < len) {
  565. sum += chunks[end].length;
  566. if (sum > WRITE_LIMIT_TOTAL) break;
  567. end++;
  568. }
  569. while (i < end - 1) {
  570. stream.write(chunks[i++]);
  571. }
  572. stream.write(chunks[i++], batchWrite);
  573. };
  574. batchWrite();
  575. }
  576. );
  577. if (name) {
  578. allWrittenFiles.add(file);
  579. if (writtenFiles !== undefined) writtenFiles.add(name);
  580. }
  581. };
  582. resolve(
  583. serialize(
  584. this,
  585. data,
  586. false,
  587. writeFile,
  588. this._hashFunction,
  589. retainedFiles
  590. ).then(async ({ backgroundJob }) => {
  591. await backgroundJob;
  592. // Rename the index file to disallow access during inconsistent file state
  593. await new Promise(
  594. /**
  595. * Handles the callback logic for this hook.
  596. * @param {(value?: undefined) => void} resolve resolve
  597. */
  598. (resolve) => {
  599. this.fs.rename(filename, `${filename}.old`, (_err) => {
  600. resolve();
  601. });
  602. }
  603. );
  604. // update all written files
  605. await Promise.all(
  606. Array.from(
  607. allWrittenFiles,
  608. (file) =>
  609. new Promise(
  610. /**
  611. * Handles the callback logic for this hook.
  612. * @param {(value?: undefined) => void} resolve resolve
  613. * @param {(reason?: Error | null) => void} reject reject
  614. * @returns {void}
  615. */
  616. (resolve, reject) => {
  617. this.fs.rename(`${file}_`, file, (err) => {
  618. if (err) return reject(err);
  619. resolve();
  620. });
  621. }
  622. )
  623. )
  624. );
  625. // As final step automatically update the index file to have a consistent pack again
  626. await new Promise(
  627. /**
  628. * Handles the callback logic for this hook.
  629. * @param {(value?: undefined) => void} resolve resolve
  630. * @returns {void}
  631. */
  632. (resolve) => {
  633. this.fs.rename(`${filename}_`, filename, (err) => {
  634. if (err) return reject(err);
  635. resolve();
  636. });
  637. }
  638. );
  639. return /** @type {true} */ (true);
  640. })
  641. );
  642. });
  643. });
  644. }
  645. /**
  646. * Restores this instance from the provided deserializer context.
  647. * @param {SerializedType} data data
  648. * @param {Context} context context object
  649. * @returns {DeserializedType | Promise<DeserializedType>} deserialized data
  650. */
  651. deserialize(data, context) {
  652. const { filename, extension = "" } = context;
  653. /**
  654. * Returns result.
  655. * @param {string | boolean} name name
  656. * @returns {Promise<Buffer[]>} result
  657. */
  658. const readFile = (name) =>
  659. new Promise((resolve, reject) => {
  660. const file = name
  661. ? join(this.fs, filename, `../${name}${extension}`)
  662. : filename;
  663. this.fs.stat(file, (err, stats) => {
  664. if (err) {
  665. reject(err);
  666. return;
  667. }
  668. let remaining = /** @type {IStats} */ (stats).size;
  669. /** @type {Buffer | undefined} */
  670. let currentBuffer;
  671. /** @type {number | undefined} */
  672. let currentBufferUsed;
  673. /** @type {Buffer[]} */
  674. const buf = [];
  675. /** @type {import("zlib").Zlib & import("stream").Transform | undefined} */
  676. let decompression;
  677. if (file.endsWith(".gz")) {
  678. decompression = createGunzip({
  679. chunkSize: DECOMPRESSION_CHUNK_SIZE
  680. });
  681. } else if (file.endsWith(".br")) {
  682. decompression = createBrotliDecompress({
  683. chunkSize: DECOMPRESSION_CHUNK_SIZE
  684. });
  685. } else if (file.endsWith(".zst")) {
  686. decompression = createZstdDecompress({
  687. chunkSize: DECOMPRESSION_CHUNK_SIZE
  688. });
  689. }
  690. if (decompression) {
  691. /** @typedef {(value: Buffer[] | PromiseLike<Buffer[]>) => void} NewResolve */
  692. /** @typedef {(reason?: Error) => void} NewReject */
  693. /** @type {NewResolve | undefined} */
  694. let newResolve;
  695. /** @type {NewReject | undefined} */
  696. let newReject;
  697. resolve(
  698. Promise.all([
  699. new Promise((rs, rj) => {
  700. newResolve = rs;
  701. newReject = rj;
  702. }),
  703. new Promise(
  704. /**
  705. * Handles the chunk size callback for this hook.
  706. * @param {(value?: undefined) => void} resolve resolve
  707. * @param {(reason?: Error) => void} reject reject
  708. */
  709. (resolve, reject) => {
  710. decompression.on("data", (chunk) => buf.push(chunk));
  711. decompression.on("end", () => resolve());
  712. decompression.on("error", (err) => reject(err));
  713. }
  714. )
  715. ]).then(() => buf)
  716. );
  717. resolve = /** @type {NewResolve} */ (newResolve);
  718. reject = /** @type {NewReject} */ (newReject);
  719. }
  720. this.fs.open(file, "r", (err, _fd) => {
  721. if (err) {
  722. reject(err);
  723. return;
  724. }
  725. const fd = /** @type {number} */ (_fd);
  726. const read = () => {
  727. if (currentBuffer === undefined) {
  728. currentBuffer = Buffer.allocUnsafeSlow(
  729. Math.min(
  730. constants.MAX_LENGTH,
  731. remaining,
  732. decompression ? DECOMPRESSION_CHUNK_SIZE : Infinity
  733. )
  734. );
  735. currentBufferUsed = 0;
  736. }
  737. let readBuffer = currentBuffer;
  738. let readOffset = /** @type {number} */ (currentBufferUsed);
  739. let readLength =
  740. currentBuffer.length -
  741. /** @type {number} */ (currentBufferUsed);
  742. // values passed to fs.read must be valid int32 values
  743. if (readOffset > 0x7fffffff) {
  744. readBuffer = currentBuffer.subarray(readOffset);
  745. readOffset = 0;
  746. }
  747. if (readLength > 0x7fffffff) {
  748. readLength = 0x7fffffff;
  749. }
  750. this.fs.read(
  751. fd,
  752. readBuffer,
  753. readOffset,
  754. readLength,
  755. null,
  756. (err, bytesRead) => {
  757. if (err) {
  758. this.fs.close(fd, () => {
  759. reject(err);
  760. });
  761. return;
  762. }
  763. /** @type {number} */
  764. (currentBufferUsed) += bytesRead;
  765. remaining -= bytesRead;
  766. if (
  767. currentBufferUsed ===
  768. /** @type {Buffer} */
  769. (currentBuffer).length
  770. ) {
  771. if (decompression) {
  772. decompression.write(currentBuffer);
  773. } else {
  774. buf.push(
  775. /** @type {Buffer} */
  776. (currentBuffer)
  777. );
  778. }
  779. currentBuffer = undefined;
  780. if (remaining === 0) {
  781. if (decompression) {
  782. decompression.end();
  783. }
  784. this.fs.close(fd, (err) => {
  785. if (err) {
  786. reject(err);
  787. return;
  788. }
  789. resolve(buf);
  790. });
  791. return;
  792. }
  793. }
  794. read();
  795. }
  796. );
  797. };
  798. read();
  799. });
  800. });
  801. });
  802. return deserialize(this, false, readFile);
  803. }
  804. }
  805. /**
  806. * Extracts the file names referenced by lazy pointer sections from serialized content.
  807. * @param {Buffer} buf decompressed file content
  808. * @returns {string[]} referenced file names (without extension)
  809. */
  810. const parsePointerNames = (buf) => {
  811. const version = buf.readUInt32LE(0);
  812. if (version !== VERSION) {
  813. throw new Error(`Invalid file version ${version}`);
  814. }
  815. const sectionCount = buf.readUInt32LE(4);
  816. let offset = 8 + sectionCount * 4;
  817. // a corrupt section table must abort the walk, never silently drop a name
  818. if (offset > buf.length) {
  819. throw new Error(`Invalid section count ${sectionCount}`);
  820. }
  821. /** @type {string[]} */
  822. const names = [];
  823. for (let i = 0; i < sectionCount; i++) {
  824. const length = buf.readInt32LE(8 + i * 4);
  825. if (length < 0) {
  826. // pointer section: u64 size + utf-8 file name
  827. const end = offset - length;
  828. if (end > buf.length) {
  829. throw new Error("Truncated pointer section");
  830. }
  831. names.push(buf.toString("utf8", offset + 8, end));
  832. offset = end;
  833. } else {
  834. offset += length;
  835. }
  836. }
  837. if (offset !== buf.length) {
  838. throw new Error("Section table does not match file size");
  839. }
  840. return names;
  841. };
  842. /**
  843. * Reads the pointer names of a compressed file by decompressing it fully
  844. * (compressed content cannot be read by byte range).
  845. * @param {IntermediateFileSystem} fs a file system
  846. * @param {string} file absolute path of the serialized file
  847. * @returns {Promise<string[]>} referenced file names (without extension)
  848. */
  849. const getReferencedFilenamesCompressed = (fs, file) =>
  850. new Promise((resolve, reject) => {
  851. fs.readFile(file, (err, rawContent) => {
  852. if (err) return reject(err);
  853. /**
  854. * Parses the decompressed content.
  855. * @param {Error | null} err error
  856. * @param {Buffer=} content decompressed content
  857. * @returns {void}
  858. */
  859. const onContent = (err, content) => {
  860. if (err) return reject(err);
  861. try {
  862. resolve(parsePointerNames(/** @type {Buffer} */ (content)));
  863. } catch (err_) {
  864. reject(/** @type {Error} */ (err_));
  865. }
  866. };
  867. const buf = /** @type {Buffer} */ (rawContent);
  868. if (file.endsWith(".gz")) {
  869. gunzip(buf, onContent);
  870. } else if (file.endsWith(".br")) {
  871. brotliDecompress(buf, onContent);
  872. } else {
  873. if (!zstdDecompress) {
  874. return reject(
  875. new Error("zstd decompression requires Node.js >= 22.15.0")
  876. );
  877. }
  878. zstdDecompress(buf, onContent);
  879. }
  880. });
  881. });
  882. /**
  883. * Reads the pointer names of an uncompressed file by reading only the header
  884. * and the pointer sections instead of the whole file.
  885. * @param {IntermediateFileSystem} fs a file system
  886. * @param {string} file absolute path of the serialized file
  887. * @returns {Promise<string[]>} referenced file names (without extension)
  888. */
  889. const getReferencedFilenamesUncompressed = (fs, file) =>
  890. new Promise((resolve, reject) => {
  891. fs.open(file, "r", (err, _fd) => {
  892. if (err) return reject(err);
  893. const fd = /** @type {number} */ (_fd);
  894. /**
  895. * Closes the file descriptor and rejects.
  896. * @param {Error} err error
  897. */
  898. const fail = (err) => {
  899. fs.close(fd, () => reject(err));
  900. };
  901. /**
  902. * Reads exactly `length` bytes at `position`.
  903. * @param {number} position file position
  904. * @param {number} length byte count
  905. * @param {(buffer: Buffer) => void} callback called with the filled buffer
  906. * @returns {void}
  907. */
  908. const readAt = (position, length, callback) => {
  909. // corrupt headers can request absurd sizes; must reject, not crash
  910. if (length > MAX_HEADER_OR_POINTER_SIZE) {
  911. return fail(new Error(`Invalid section size ${length} in ${file}`));
  912. }
  913. /** @type {Buffer} */
  914. let buffer;
  915. try {
  916. buffer = Buffer.allocUnsafe(length);
  917. } catch (err) {
  918. return fail(/** @type {Error} */ (err));
  919. }
  920. let bytesDone = 0;
  921. const readMore = () => {
  922. fs.read(
  923. fd,
  924. buffer,
  925. bytesDone,
  926. length - bytesDone,
  927. position + bytesDone,
  928. (err, bytesRead) => {
  929. if (err) return fail(err);
  930. if (bytesRead === 0) {
  931. return fail(new Error(`Unexpected end of file ${file}`));
  932. }
  933. bytesDone += bytesRead;
  934. if (bytesDone < length) return readMore();
  935. callback(buffer);
  936. }
  937. );
  938. };
  939. readMore();
  940. };
  941. readAt(0, 8, (header) => {
  942. const version = header.readUInt32LE(0);
  943. if (version !== VERSION) {
  944. return fail(new Error(`Invalid file version ${version}`));
  945. }
  946. const sectionCount = header.readUInt32LE(4);
  947. if (sectionCount === 0) {
  948. return fs.close(fd, (err) => (err ? reject(err) : resolve([])));
  949. }
  950. readAt(8, sectionCount * 4, (lengthsBuffer) => {
  951. /** @type {{ position: number, length: number }[]} */
  952. const pointerSections = [];
  953. let position = 8 + sectionCount * 4;
  954. for (let i = 0; i < sectionCount; i++) {
  955. const length = lengthsBuffer.readInt32LE(i * 4);
  956. if (length < 0) {
  957. pointerSections.push({ position, length: -length });
  958. position -= length;
  959. } else {
  960. position += length;
  961. }
  962. }
  963. // the section table must account for exactly the whole file,
  964. // otherwise pointer reads land on wrong bytes and under-report
  965. fs.stat(file, (err, stats) => {
  966. if (err) return fail(err);
  967. const stat = /** @type {import("../util/fs").IStats} */ (stats);
  968. if (stat.size !== position) {
  969. return fail(
  970. new Error(`Section table does not match size of ${file}`)
  971. );
  972. }
  973. /** @type {string[]} */
  974. const names = [];
  975. /**
  976. * Reads the pointer section at `index`, then the next one.
  977. * @param {number} index pointer section index
  978. * @returns {void}
  979. */
  980. const readNext = (index) => {
  981. if (index >= pointerSections.length) {
  982. return fs.close(fd, (err) =>
  983. err ? reject(err) : resolve(names)
  984. );
  985. }
  986. const section = pointerSections[index];
  987. readAt(section.position, section.length, (buffer) => {
  988. // pointer section: u64 size + utf-8 file name
  989. names.push(buffer.toString("utf8", 8));
  990. readNext(index + 1);
  991. });
  992. };
  993. readNext(0);
  994. });
  995. });
  996. });
  997. });
  998. });
  999. /**
  1000. * Reads the file names referenced by a serialized file (its lazy pointer sections)
  1001. * without deserializing its content. Referenced files may reference further files.
  1002. * @param {IntermediateFileSystem} fs a file system
  1003. * @param {string} file absolute path of the serialized file
  1004. * @returns {Promise<string[]>} referenced file names (without extension)
  1005. */
  1006. const getReferencedFilenames = (fs, file) =>
  1007. file.endsWith(".gz") || file.endsWith(".br") || file.endsWith(".zst")
  1008. ? getReferencedFilenamesCompressed(fs, file)
  1009. : getReferencedFilenamesUncompressed(fs, file);
  1010. // Exposed for testing the content-buffer boundary handling in `deserialize`.
  1011. FileMiddleware._deserialize = deserialize;
  1012. FileMiddleware.getReferencedFilenames = getReferencedFilenames;
  1013. module.exports = FileMiddleware;