Add an abstract BasePDFStreamRangeReader class, that all the old IPDFStreamRangeReader implementations inherit from

Given that there's no less than *five* different, but very similar, implementations this helps reduce code duplication and simplifies maintenance.
This commit is contained in:
Jonas Jenwald 2026-01-30 08:02:07 +01:00
parent 54d8c5e7b4
commit 4ca205bac3
8 changed files with 116 additions and 101 deletions

View File

@ -15,6 +15,7 @@
import { import {
BasePDFStream, BasePDFStream,
BasePDFStreamRangeReader,
BasePDFStreamReader, BasePDFStreamReader,
} from "../shared/base_pdf_stream.js"; } from "../shared/base_pdf_stream.js";
@ -58,9 +59,11 @@ class PDFWorkerStreamReader extends BasePDFStreamReader {
} }
} }
/** @implements {IPDFStreamRangeReader} */ class PDFWorkerStreamRangeReader extends BasePDFStreamRangeReader {
class PDFWorkerStreamRangeReader { _reader = null;
constructor(stream, begin, end) { constructor(stream, begin, end) {
super(stream, begin, end);
const { msgHandler } = stream._source; const { msgHandler } = stream._source;
const readableStream = msgHandler.sendWithStream("GetRangeReader", { const readableStream = msgHandler.sendWithStream("GetRangeReader", {

View File

@ -16,15 +16,16 @@
import { AbortException, warn } from "../shared/util.js"; import { AbortException, warn } from "../shared/util.js";
import { import {
BasePDFStream, BasePDFStream,
BasePDFStreamRangeReader,
BasePDFStreamReader, BasePDFStreamReader,
} from "../shared/base_pdf_stream.js"; } from "../shared/base_pdf_stream.js";
import { import {
createHeaders, createHeaders,
createResponseError, createResponseError,
ensureResponseOrigin,
extractFilenameFromHeader, extractFilenameFromHeader,
getResponseOrigin, getResponseOrigin,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
validateResponseStatus,
} from "./network_utils.js"; } from "./network_utils.js";
if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) { if (typeof PDFJSDev !== "undefined" && PDFJSDev.test("MOZCENTRAL")) {
@ -44,6 +45,12 @@ function fetchUrl(url, headers, withCredentials, abortController) {
}); });
} }
function ensureResponseStatus(status, url) {
if (status !== 200 && status !== 206) {
throw createResponseError(status, url);
}
}
function getArrayBuffer(val) { function getArrayBuffer(val) {
if (val instanceof Uint8Array) { if (val instanceof Uint8Array) {
return val.buffer; return val.buffer;
@ -91,9 +98,7 @@ class PDFFetchStreamReader extends BasePDFStreamReader {
.then(response => { .then(response => {
stream._responseOrigin = getResponseOrigin(response.url); stream._responseOrigin = getResponseOrigin(response.url);
if (!validateResponseStatus(response.status)) { ensureResponseStatus(response.status, url);
throw createResponseError(response.status, url);
}
this._reader = response.body.getReader(); this._reader = response.body.getReader();
const responseHeaders = response.headers; const responseHeaders = response.headers;
@ -144,35 +149,30 @@ class PDFFetchStreamReader extends BasePDFStreamReader {
} }
} }
/** @implements {IPDFStreamRangeReader} */ class PDFFetchStreamRangeReader extends BasePDFStreamRangeReader {
class PDFFetchStreamRangeReader { _abortController = new AbortController();
constructor(stream, begin, end) {
this._stream = stream; _readCapability = Promise.withResolvers();
this._reader = null;
const source = stream._source; _reader = null;
this._withCredentials = source.withCredentials || false;
this._readCapability = Promise.withResolvers(); constructor(stream, begin, end) {
super(stream, begin, end);
const { url, withCredentials } = stream._source;
this._abortController = new AbortController();
// Always create a copy of the headers. // Always create a copy of the headers.
const headers = new Headers(stream.headers); const headers = new Headers(stream.headers);
headers.append("Range", `bytes=${begin}-${end - 1}`); headers.append("Range", `bytes=${begin}-${end - 1}`);
const url = source.url; fetchUrl(url, headers, withCredentials, this._abortController)
fetchUrl(url, headers, this._withCredentials, this._abortController)
.then(response => { .then(response => {
const responseOrigin = getResponseOrigin(response.url); const responseOrigin = getResponseOrigin(response.url);
if (responseOrigin !== stream._responseOrigin) { ensureResponseOrigin(responseOrigin, stream._responseOrigin);
throw new Error( ensureResponseStatus(response.status, url);
`Expected range response-origin "${responseOrigin}" to match "${stream._responseOrigin}".`
);
}
if (!validateResponseStatus(response.status)) {
throw createResponseError(response.status, url);
}
this._readCapability.resolve();
this._reader = response.body.getReader(); this._reader = response.body.getReader();
this._readCapability.resolve();
}) })
.catch(this._readCapability.reject); .catch(this._readCapability.reject);
} }

View File

@ -16,11 +16,13 @@
import { assert, stringToBytes, warn } from "../shared/util.js"; import { assert, stringToBytes, warn } from "../shared/util.js";
import { import {
BasePDFStream, BasePDFStream,
BasePDFStreamRangeReader,
BasePDFStreamReader, BasePDFStreamReader,
} from "../shared/base_pdf_stream.js"; } from "../shared/base_pdf_stream.js";
import { import {
createHeaders, createHeaders,
createResponseError, createResponseError,
ensureResponseOrigin,
extractFilenameFromHeader, extractFilenameFromHeader,
getResponseOrigin, getResponseOrigin,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
@ -307,57 +309,59 @@ class PDFNetworkStreamReader extends BasePDFStreamReader {
} }
} }
/** @implements {IPDFStreamRangeReader} */ class PDFNetworkStreamRangeReader extends BasePDFStreamRangeReader {
class PDFNetworkStreamRangeReader {
onClosed = null; onClosed = null;
_done = false;
_queuedChunk = null;
_requests = [];
_storedError = null;
constructor(stream, begin, end) { constructor(stream, begin, end) {
this._stream = stream; super(stream, begin, end);
this._requestXhr = stream._request({ this._requestXhr = stream._request({
begin, begin,
end, end,
onHeadersReceived: this._onHeadersReceived.bind(this), onHeadersReceived: this.#onHeadersReceived.bind(this),
onDone: this._onDone.bind(this), onDone: this.#onDone.bind(this),
onError: this._onError.bind(this), onError: this.#onError.bind(this),
onProgress: null, onProgress: null,
}); });
this._requests = [];
this._queuedChunk = null;
this._done = false;
this._storedError = undefined;
} }
_onHeadersReceived() { #onHeadersReceived() {
const responseOrigin = getResponseOrigin(this._requestXhr?.responseURL); const responseOrigin = getResponseOrigin(this._requestXhr?.responseURL);
try {
if (responseOrigin !== this._stream._responseOrigin) { ensureResponseOrigin(responseOrigin, this._stream._responseOrigin);
this._storedError = new Error( } catch (ex) {
`Expected range response-origin "${responseOrigin}" to match "${this._stream._responseOrigin}".` this._storedError = ex;
); this.#onError(0);
this._onError(0);
} }
} }
_onDone(chunk) { #onDone(chunk) {
if (this._requests.length > 0) { if (this._requests.length > 0) {
const requestCapability = this._requests.shift(); const capability = this._requests.shift();
requestCapability.resolve({ value: chunk, done: false }); capability.resolve({ value: chunk, done: false });
} else { } else {
this._queuedChunk = chunk; this._queuedChunk = chunk;
} }
this._done = true; this._done = true;
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.resolve({ value: undefined, done: true }); capability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
this.onClosed?.(); this.onClosed?.();
} }
_onError(status) { #onError(status) {
this._storedError ??= createResponseError(status, this._stream.url); this._storedError ??= createResponseError(status, this._stream.url);
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.reject(this._storedError); capability.reject(this._storedError);
} }
this._requests.length = 0; this._requests.length = 0;
this._queuedChunk = null; this._queuedChunk = null;
@ -375,15 +379,15 @@ class PDFNetworkStreamRangeReader {
if (this._done) { if (this._done) {
return { value: undefined, done: true }; return { value: undefined, done: true };
} }
const requestCapability = Promise.withResolvers(); const capability = Promise.withResolvers();
this._requests.push(requestCapability); this._requests.push(capability);
return requestCapability.promise; return capability.promise;
} }
cancel(reason) { cancel(reason) {
this._done = true; this._done = true;
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.resolve({ value: undefined, done: true }); capability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;

View File

@ -107,15 +107,19 @@ function createResponseError(status, url) {
); );
} }
function validateResponseStatus(status) { function ensureResponseOrigin(rangeOrigin, origin) {
return status === 200 || status === 206; if (rangeOrigin !== origin) {
throw new Error(
`Expected range response-origin "${rangeOrigin}" to match "${origin}".`
);
}
} }
export { export {
createHeaders, createHeaders,
createResponseError, createResponseError,
ensureResponseOrigin,
extractFilenameFromHeader, extractFilenameFromHeader,
getResponseOrigin, getResponseOrigin,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
validateResponseStatus,
}; };

View File

@ -17,6 +17,7 @@
import { AbortException, assert, warn } from "../shared/util.js"; import { AbortException, assert, warn } from "../shared/util.js";
import { import {
BasePDFStream, BasePDFStream,
BasePDFStreamRangeReader,
BasePDFStreamReader, BasePDFStreamReader,
} from "../shared/base_pdf_stream.js"; } from "../shared/base_pdf_stream.js";
import { createResponseError } from "./network_utils.js"; import { createResponseError } from "./network_utils.js";
@ -66,7 +67,7 @@ function getArrayBuffer(val) {
class PDFNodeStream extends BasePDFStream { class PDFNodeStream extends BasePDFStream {
constructor(source) { constructor(source) {
super(source, PDFNodeStreamReader, PDFNodeStreamFsRangeReader); super(source, PDFNodeStreamReader, PDFNodeStreamRangeReader);
this.url = parseUrlOrPath(source.url); this.url = parseUrlOrPath(source.url);
assert( assert(
this.url.protocol === "file:", this.url.protocol === "file:",
@ -142,12 +143,14 @@ class PDFNodeStreamReader extends BasePDFStreamReader {
} }
} }
class PDFNodeStreamFsRangeReader { class PDFNodeStreamRangeReader extends BasePDFStreamRangeReader {
_readCapability = Promise.withResolvers(); _readCapability = Promise.withResolvers();
_reader = null; _reader = null;
constructor(stream, begin, end) { constructor(stream, begin, end) {
super(stream, begin, end);
const url = stream.url; const url = stream.url;
const fs = process.getBuiltinModule("fs"); const fs = process.getBuiltinModule("fs");
try { try {

View File

@ -13,11 +13,9 @@
* limitations under the License. * limitations under the License.
*/ */
// eslint-disable-next-line max-len
/** @typedef {import("../interfaces").IPDFStreamRangeReader} IPDFStreamRangeReader */
import { import {
BasePDFStream, BasePDFStream,
BasePDFStreamRangeReader,
BasePDFStreamReader, BasePDFStreamReader,
} from "../shared/base_pdf_stream.js"; } from "../shared/base_pdf_stream.js";
import { assert } from "../shared/util.js"; import { assert } from "../shared/util.js";
@ -187,17 +185,20 @@ class PDFDataTransportStreamReader extends BasePDFStreamReader {
} }
} }
/** @implements {IPDFStreamRangeReader} */ class PDFDataTransportStreamRangeReader extends BasePDFStreamRangeReader {
class PDFDataTransportStreamRangeReader {
onDone = null; onDone = null;
_begin = -1;
_done = false;
_queuedChunk = null;
_requests = [];
constructor(stream, begin, end) { constructor(stream, begin, end) {
this._stream = stream; super(stream, begin, end);
this._begin = begin; this._begin = begin;
this._end = end;
this._queuedChunk = null;
this._requests = [];
this._done = false;
} }
_enqueue(chunk) { _enqueue(chunk) {
@ -207,10 +208,11 @@ class PDFDataTransportStreamRangeReader {
if (this._requests.length === 0) { if (this._requests.length === 0) {
this._queuedChunk = chunk; this._queuedChunk = chunk;
} else { } else {
const requestsCapability = this._requests.shift(); const firstCapability = this._requests.shift();
requestsCapability.resolve({ value: chunk, done: false }); firstCapability.resolve({ value: chunk, done: false });
for (const requestCapability of this._requests) {
requestCapability.resolve({ value: undefined, done: true }); for (const capability of this._requests) {
capability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
} }
@ -227,15 +229,15 @@ class PDFDataTransportStreamRangeReader {
if (this._done) { if (this._done) {
return { value: undefined, done: true }; return { value: undefined, done: true };
} }
const requestCapability = Promise.withResolvers(); const capability = Promise.withResolvers();
this._requests.push(requestCapability); this._requests.push(capability);
return requestCapability.promise; return capability.promise;
} }
cancel(reason) { cancel(reason) {
this._done = true; this._done = true;
for (const requestCapability of this._requests) { for (const capability of this._requests) {
requestCapability.resolve({ value: undefined, done: true }); capability.resolve({ value: undefined, done: true });
} }
this._requests.length = 0; this._requests.length = 0;
this.onDone?.(); this.onDone?.();

View File

@ -198,10 +198,20 @@ class BasePDFStreamReader {
/** /**
* Interface for a PDF binary data fragment reader. * Interface for a PDF binary data fragment reader.
*
* @interface
*/ */
class IPDFStreamRangeReader { class BasePDFStreamRangeReader {
_stream = null;
constructor(stream, begin, end) {
if (
(typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) &&
this.constructor === BasePDFStreamRangeReader
) {
unreachable("Cannot initialize BasePDFStreamRangeReader.");
}
this._stream = stream;
}
/** /**
* Requests a chunk of the binary data. The method returns the promise, which * Requests a chunk of the binary data. The method returns the promise, which
* is resolved into object with properties "value" and "done". If the done * is resolved into object with properties "value" and "done". If the done
@ -210,13 +220,17 @@ class IPDFStreamRangeReader {
* set to true. * set to true.
* @returns {Promise} * @returns {Promise}
*/ */
async read() {} async read() {
unreachable("Abstract method `read` called");
}
/** /**
* Cancels all pending read requests and closes the stream. * Cancels all pending read requests and closes the stream.
* @param {Object} reason * @param {Object} reason
*/ */
cancel(reason) {} cancel(reason) {
unreachable("Abstract method `cancel` called");
}
} }
export { BasePDFStream, BasePDFStreamReader, IPDFStreamRangeReader }; export { BasePDFStream, BasePDFStreamRangeReader, BasePDFStreamReader };

View File

@ -18,7 +18,6 @@ import {
createResponseError, createResponseError,
extractFilenameFromHeader, extractFilenameFromHeader,
validateRangeRequestCapabilities, validateRangeRequestCapabilities,
validateResponseStatus,
} from "../../src/display/network_utils.js"; } from "../../src/display/network_utils.js";
import { ResponseException } from "../../src/shared/util.js"; import { ResponseException } from "../../src/shared/util.js";
@ -391,18 +390,4 @@ describe("network_utils", function () {
testCreateResponseError("https://foo.com/bar.pdf", 0, false); testCreateResponseError("https://foo.com/bar.pdf", 0, false);
}); });
}); });
describe("validateResponseStatus", function () {
it("accepts valid response statuses", function () {
expect(validateResponseStatus(200)).toEqual(true);
expect(validateResponseStatus(206)).toEqual(true);
});
it("rejects invalid response statuses", function () {
expect(validateResponseStatus(302)).toEqual(false);
expect(validateResponseStatus(404)).toEqual(false);
expect(validateResponseStatus(null)).toEqual(false);
expect(validateResponseStatus(undefined)).toEqual(false);
});
});
}); });