Skip to content

Commit d890ac8

Browse files
paulirishDevtools-frontend LUCI CQ
authored andcommitted
Define common gzip helper methods
Also we no longer need to define the interfaces as they're now in @types/node/stream/web.d.ts Bug:432043263 Change-Id: I268a0b377457e71cb6528a436ef1369572853d2d Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/6761794 Commit-Queue: Paul Irish <[email protected]> Reviewed-by: Connor Clark <[email protected]> Auto-Submit: Paul Irish <[email protected]>
1 parent 7bb6e09 commit d890ac8

File tree

12 files changed

+169
-142
lines changed

12 files changed

+169
-142
lines changed

config/gni/devtools_grd_files.gni

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -812,6 +812,7 @@ grd_files_unbundled_sources = [
812812
"front_end/core/common/Console.js",
813813
"front_end/core/common/Debouncer.js",
814814
"front_end/core/common/EventTarget.js",
815+
"front_end/core/common/Gzip.js",
815816
"front_end/core/common/JavaScriptMetaData.js",
816817
"front_end/core/common/Lazy.js",
817818
"front_end/core/common/Linkifier.js",

front_end/core/common/BUILD.gn

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ devtools_module("common") {
2121
"Console.ts",
2222
"Debouncer.ts",
2323
"EventTarget.ts",
24+
"Gzip.ts",
2425
"JavaScriptMetaData.ts",
2526
"Lazy.ts",
2627
"Linkifier.ts",
@@ -83,6 +84,7 @@ ts_library("unittests") {
8384
"Console.test.ts",
8485
"Debouncer.test.ts",
8586
"EventTarget.test.ts",
87+
"Gzip.test.ts",
8688
"Lazy.test.ts",
8789
"MapWithDefault.test.ts",
8890
"Mutex.test.ts",

front_end/core/common/Gzip.test.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import * as Common from './common.js';
6+
7+
describe('Gzip', () => {
8+
it('can compress and decompress a string', async () => {
9+
const text = 'Hello, world!';
10+
const compressed = await Common.Gzip.compress(text);
11+
const decompressed = await Common.Gzip.decompress(compressed);
12+
assert.strictEqual(decompressed, text);
13+
});
14+
15+
it('can compress and decompress a stream', async () => {
16+
const text = 'Hello, world! This is a stream test.';
17+
const textEncoder = new TextEncoder();
18+
const inputStream = new ReadableStream({
19+
start(controller) {
20+
controller.enqueue(textEncoder.encode(text));
21+
controller.close();
22+
},
23+
});
24+
25+
const compressedStream = Common.Gzip.compressStream(inputStream);
26+
const decompressedStream = Common.Gzip.decompressStream(compressedStream);
27+
28+
const buffer = await new Response(decompressedStream).arrayBuffer();
29+
const decodedText = new TextDecoder().decode(buffer);
30+
31+
assert.strictEqual(decodedText, text);
32+
});
33+
});
34+
35+
describe('arrayBufferToString', () => {
36+
it('can decompress a gzipped buffer', async () => {
37+
const text = 'Hello, world!';
38+
const compressed = await Common.Gzip.compress(text);
39+
const result = await Common.Gzip.arrayBufferToString(compressed);
40+
assert.strictEqual(result, text);
41+
});
42+
it('can decode a plaintext buffer', async () => {
43+
const text = 'Hello, buddy!';
44+
const buffer = new TextEncoder().encode(text).buffer as ArrayBuffer;
45+
const result = await Common.Gzip.arrayBufferToString(buffer);
46+
assert.strictEqual(result, text);
47+
});
48+
});
49+
50+
describe('fileToString', () => {
51+
it('can decompress a gzipped file', async () => {
52+
const text = '{"key": "value"}';
53+
const compressed = await Common.Gzip.compress(text);
54+
const result = await Common.Gzip.fileToString(new File([compressed], 'file.json.gz', {type: 'application/gzip'}));
55+
assert.strictEqual(result, text);
56+
});
57+
it('can decode a plaintext file', async () => {
58+
const text = 'Hello, buddy!';
59+
const file = new File([text], 'test.txt');
60+
const result = await Common.Gzip.fileToString(file);
61+
assert.strictEqual(result, text);
62+
});
63+
});

front_end/core/common/Gzip.ts

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
// Copyright 2025 The Chromium Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
/**
6+
* Quickly determine if gzipped, by seeing if the first 3 bytes of the file header match the gzip signature
7+
*/
8+
export function isGzip(ab: ArrayBuffer): boolean {
9+
const buf = new Uint8Array(ab);
10+
if (!buf || buf.length < 3) {
11+
return false;
12+
}
13+
// https://www.rfc-editor.org/rfc/rfc1952#page-6
14+
return buf[0] === 0x1F && buf[1] === 0x8B && buf[2] === 0x08;
15+
}
16+
17+
/** Decode a gzipped _or_ plain text ArrayBuffer to a decoded string */
18+
export async function arrayBufferToString(ab: ArrayBuffer): Promise<string> {
19+
if (isGzip(ab)) {
20+
return await decompress(ab);
21+
}
22+
const str = new TextDecoder('utf-8').decode(ab);
23+
return str;
24+
}
25+
26+
export async function fileToString(file: File): Promise<string> {
27+
let stream = file.stream();
28+
if (file.type.endsWith('gzip')) {
29+
stream = decompressStream(stream);
30+
}
31+
const arrayBuffer = await new Response(stream).arrayBuffer();
32+
const str = new TextDecoder('utf-8').decode(arrayBuffer);
33+
return str;
34+
}
35+
36+
/**
37+
* Decompress a gzipped ArrayBuffer to a string.
38+
* Consider using `arrayBufferToString` instead, which can handle both gzipped and plain text buffers.
39+
*/
40+
export async function decompress(gzippedBuffer: ArrayBuffer): Promise<string> {
41+
const buffer = await gzipCodec(gzippedBuffer, new DecompressionStream('gzip'));
42+
const str = new TextDecoder('utf-8').decode(buffer);
43+
return str;
44+
}
45+
export async function compress(str: string): Promise<ArrayBuffer> {
46+
const encoded = new TextEncoder().encode(str);
47+
const buffer = await gzipCodec(encoded, new CompressionStream('gzip'));
48+
return buffer;
49+
}
50+
51+
// Private coder/decoder
52+
function gzipCodec(buffer: Uint8Array<ArrayBufferLike>|ArrayBuffer, codecStream: CompressionStream|DecompressionStream):
53+
Promise<ArrayBuffer> {
54+
const {readable, writable} = new TransformStream();
55+
const codecReadable = readable.pipeThrough(codecStream);
56+
57+
const writer = writable.getWriter();
58+
void writer.write(buffer);
59+
void writer.close();
60+
// A response is a convenient way to get an ArrayBuffer from a ReadableStream.
61+
return new Response(codecReadable).arrayBuffer();
62+
}
63+
64+
export function decompressStream(stream: ReadableStream): ReadableStream {
65+
// https://github.com/wicg/compression/blob/main/explainer.md#deflate-compress-an-arraybuffer
66+
const ds = new DecompressionStream('gzip');
67+
return stream.pipeThrough(ds);
68+
}
69+
export function compressStream(stream: ReadableStream): ReadableStream {
70+
const cs = new CompressionStream('gzip');
71+
return stream.pipeThrough(cs);
72+
}

front_end/core/common/common.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import * as ColorUtils from './ColorUtils.js';
1212
import * as Console from './Console.js';
1313
import * as Debouncer from './Debouncer.js';
1414
import * as EventTarget from './EventTarget.js';
15+
import * as Gzip from './Gzip.js';
1516
import * as JavaScriptMetaData from './JavaScriptMetaData.js';
1617
import * as Lazy from './Lazy.js';
1718
import * as Linkifier from './Linkifier.js';
@@ -52,6 +53,7 @@ export {
5253
Console,
5354
Debouncer,
5455
EventTarget,
56+
Gzip,
5557
JavaScriptMetaData,
5658
Lazy,
5759
Linkifier,

front_end/core/host/InspectorFrontendHost.ts

Lines changed: 2 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,6 @@ import {
6363
} from './InspectorFrontendHostAPI.js';
6464
import {streamWrite as resourceLoaderStreamWrite} from './ResourceLoader.js';
6565

66-
interface DecompressionStream extends GenericTransformStream {
67-
readonly format: string;
68-
}
69-
declare const DecompressionStream: {
70-
prototype: DecompressionStream,
71-
new (format: string): DecompressionStream,
72-
};
73-
7466
const UIStrings = {
7567
/**
7668
*@description Document title in Inspector Frontend Host of the DevTools window
@@ -331,28 +323,10 @@ export class InspectorFrontendHostStub implements InspectorFrontendHostAPI {
331323

332324
loadNetworkResource(
333325
url: string, _headers: string, streamId: number, callback: (arg0: LoadNetworkResourceResult) => void): void {
334-
// Read the first 3 bytes looking for the gzip signature in the file header
335-
function isGzip(ab: ArrayBuffer): boolean {
336-
const buf = new Uint8Array(ab);
337-
if (!buf || buf.length < 3) {
338-
return false;
339-
}
340-
341-
// https://www.rfc-editor.org/rfc/rfc1952#page-6
342-
return buf[0] === 0x1F && buf[1] === 0x8B && buf[2] === 0x08;
343-
}
344326
fetch(url)
345327
.then(async result => {
346-
const resultArrayBuf = await result.arrayBuffer();
347-
let decoded: ReadableStream|ArrayBuffer = resultArrayBuf;
348-
if (isGzip(resultArrayBuf)) {
349-
const ds = new DecompressionStream('gzip');
350-
const writer = ds.writable.getWriter();
351-
void writer.write(resultArrayBuf);
352-
void writer.close();
353-
decoded = ds.readable;
354-
}
355-
const text = await new Response(decoded).text();
328+
const respBuffer = await result.arrayBuffer();
329+
const text = await Common.Gzip.arrayBufferToString(respBuffer);
356330
return text;
357331
})
358332
.then(function(text) {

front_end/models/bindings/FileUtils.test.ts

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,6 @@ import * as Bindings from './bindings.js';
99
const ChunkedFileReader = Bindings.FileUtils.ChunkedFileReader;
1010
const StringOutputStream = Common.StringOutputStream.StringOutputStream;
1111

12-
interface CompressionStream extends GenericTransformStream {
13-
readonly format: string;
14-
}
15-
declare const CompressionStream: {
16-
prototype: CompressionStream,
17-
new (format: string): CompressionStream,
18-
};
19-
2012
describe('FileUtils', () => {
2113
describe('ChunkedFileReader', () => {
2214
it('re-assembles chunks including multibyte characters', async () => {
@@ -45,8 +37,7 @@ describe('FileUtils', () => {
4537
it('can decompress gzipped data', async () => {
4638
async function getAsCompressedFile(text: string) {
4739
const blob = new Blob([text], {type: 'text/plain'});
48-
// https://github.com/wicg/compression/blob/main/explainer.md#deflate-compress-an-arraybuffer
49-
const cstream = blob.stream().pipeThrough(new CompressionStream('gzip'));
40+
const cstream = Common.Gzip.compressStream(blob.stream());
5041
const creader = cstream.getReader();
5142
const values: string[] = [];
5243

front_end/models/bindings/FileUtils.ts

Lines changed: 3 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
2929
*/
3030

31-
import type * as Common from '../../core/common/common.js';
31+
import * as Common from '../../core/common/common.js';
3232
import type * as Platform from '../../core/platform/platform.js';
3333
import * as TextUtils from '../text_utils/text_utils.js';
3434
import * as Workspace from '../workspace/workspace.js';
@@ -44,13 +44,6 @@ export interface ChunkedReader {
4444

4545
error(): DOMError|null;
4646
}
47-
interface DecompressionStream extends GenericTransformStream {
48-
readonly format: string;
49-
}
50-
declare const DecompressionStream: {
51-
prototype: DecompressionStream,
52-
new (format: string): DecompressionStream,
53-
};
5447

5548
export class ChunkedFileReader implements ChunkedReader {
5649
#file: File|null;
@@ -84,11 +77,8 @@ export class ChunkedFileReader implements ChunkedReader {
8477
}
8578

8679
if (this.#file?.type.endsWith('gzip')) {
87-
// TypeScript can't tell if to use @types/node or lib.webworker.d.ts
88-
// types, so we force it to here.
89-
// crbug.com/1392092
90-
const fileStream = this.#file.stream() as unknown as ReadableStream<Uint8Array>;
91-
const stream = this.decompressStream(fileStream);
80+
const fileStream = this.#file.stream();
81+
const stream = Common.Gzip.decompressStream(fileStream);
9282
this.#streamReader = stream.getReader();
9383
} else {
9484
this.#reader = new FileReader();
@@ -127,13 +117,6 @@ export class ChunkedFileReader implements ChunkedReader {
127117
return this.#errorInternal;
128118
}
129119

130-
// Decompress gzip natively thanks to https://wicg.github.io/compression/
131-
private decompressStream(stream: ReadableStream): ReadableStream {
132-
const ds = new DecompressionStream('gzip');
133-
const decompressionStream = stream.pipeThrough(ds);
134-
return decompressionStream;
135-
}
136-
137120
private onChunkLoaded(event: Event): void {
138121
if (this.#isCanceled) {
139122
return;

front_end/models/trace/extras/ScriptDuplication.test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,15 @@ import * as Platform from '../../../core/platform/platform.js';
66
import * as SDK from '../../../core/sdk/sdk.js';
77
import type * as Protocol from '../../../generated/protocol.js';
88
import {describeWithEnvironment, expectConsoleLogs} from '../../../testing/EnvironmentHelpers.js';
9-
import {fetchFixture} from '../../../testing/TraceLoader.js';
9+
import {fetchFileAsText} from '../../../testing/TraceLoader.js';
1010
import * as Trace from '../trace.js';
1111

1212
async function loadScriptFixture(
1313
name: string, modify?: (fixture: {content: string, sourceMapJson: SDK.SourceMap.SourceMapV3Object}) => void):
1414
Promise<Trace.Handlers.ModelHandlers.Scripts.Script> {
1515
const content =
16-
await fetchFixture(new URL(`../../../panels/timeline/fixtures/traces/scripts/${name}.js.gz`, import.meta.url));
17-
const mapText = await fetchFixture(
16+
await fetchFileAsText(new URL(`../../../panels/timeline/fixtures/traces/scripts/${name}.js.gz`, import.meta.url));
17+
const mapText = await fetchFileAsText(
1818
new URL(`../../../panels/timeline/fixtures/traces/scripts/${name}.js.map.gz`, import.meta.url));
1919
const sourceMapJson = JSON.parse(mapText) as SDK.SourceMap.SourceMapV3Object;
2020
const fixture = {content, sourceMapJson};

front_end/models/trace/insights/DuplicatedJavaScript.test.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import {describeWithEnvironment} from '../../../testing/EnvironmentHelpers.js';
66
import {getFirstOrError, getInsightOrError, processTrace} from '../../../testing/InsightHelpers.js';
7-
import {fetchFixture, TraceLoader} from '../../../testing/TraceLoader.js';
7+
import {TraceLoader} from '../../../testing/TraceLoader.js';
88
import * as Trace from '../trace.js';
99

1010
describeWithEnvironment('DuplicatedJavaScript', function() {
@@ -95,9 +95,8 @@ describeWithEnvironment('DuplicatedJavaScript', function() {
9595

9696
it('works (inline source maps in metadata)', async function() {
9797
// Load this trace in a way that mutating it is safe.
98-
const traceText = await fetchFixture(
98+
const fileContents = await TraceLoader.loadTraceFileFromURL(
9999
new URL('../../../panels/timeline/fixtures/traces/dupe-js-inline-maps.json.gz', import.meta.url));
100-
const fileContents = JSON.parse(traceText) as Trace.Types.File.TraceFile;
101100

102101
// Remove the source map data urls from the trace, and move to metadata.
103102
// This reflects how Chromium will elide data source map urls.

0 commit comments

Comments
 (0)