Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cached-hash-update-buffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"webpack-sources": patch
---

Hand out a recorded hash update as buffers, so cached data carries no big string.
21 changes: 20 additions & 1 deletion lib/CachedSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,25 @@ const mapToBufferedMap = (map) => {
return bufferedMap;
};

/**
* Encodes every string a recorded hash update holds as a buffer, so cached data
* handed to a caller carries no big string: a string goes into the serialized
* payload, where it is copied in and rebuilt on every deserialization, while a
* buffer is carried alongside it. Hashing reads a string as utf8, so the
* encoded update hashes the same. It encodes in place, so a source handed out
* repeatedly is encoded once, and one that never is keeps its string uncopied.
* @param {(string | Buffer)[]=} update recorded hash update
* @returns {(string | Buffer)[]=} the update, holding no string
*/
const bufferHashUpdate = (update) => {
if (update === undefined) return undefined;
for (let index = 0; index < update.length; index++) {
const item = update[index];
if (typeof item === "string") update[index] = Buffer.from(item, "utf8");
}
return update;
};

/**
* @param {null | BufferedMap} bufferedMap buffered map
* @returns {null | RawSourceMap} map
Expand Down Expand Up @@ -209,7 +228,7 @@ class CachedSource extends Source {
: undefined,
size: this._cachedSize,
maps: bufferedMaps,
hash: this._cachedHashUpdate,
hash: bufferHashUpdate(this._cachedHashUpdate),
};
}

Expand Down
30 changes: 30 additions & 0 deletions test/CachedSource.js
Original file line number Diff line number Diff line change
Expand Up @@ -637,4 +637,34 @@ describe.each([
expect(clone.buffer()).toEqual(source.buffer());
expect(clone.size()).toEqual(source.size());
});

it("should hand out a recorded hash update without a big string", () => {
const big = "a".repeat(200000);
const source = new CachedSource(new OriginalSource(big, "big.js"));
const internal = /** @type {{ _cachedHashUpdate: unknown[] }} */ (
/** @type {unknown} */ (source)
);

source.updateHash(crypto.createHash("md5"));

// a source nobody asks cached data of keeps its own string, uncopied
expect(internal._cachedHashUpdate).toContain(big);

const cachedData = source.getCachedData();
expect(cachedData.hash).toBeDefined();
for (const item of /** @type {(string | Buffer)[]} */ (cachedData.hash)) {
expect(typeof item).not.toBe("string");
}

// asking again encodes nothing: the recording already holds buffers
expect(source.getCachedData().hash).toBe(cachedData.hash);

// @ts-expect-error for tests
const clone = new CachedSource(null, cachedData);
const cloneHash = crypto.createHash("md5");
clone.updateHash(cloneHash);
const sourceHash = crypto.createHash("md5");
source.updateHash(sourceHash);
expect(cloneHash.digest("hex")).toBe(sourceHash.digest("hex"));
});
});
Loading