From 90b26646222df2e6092477e946cd9b3cc70f9fbf Mon Sep 17 00:00:00 2001 From: Jonas Jenwald Date: Mon, 19 Feb 2024 12:39:11 +0100 Subject: [PATCH] Add better validation for the "PREFERENCE" kind `AppOptions` Given that the "PREFERENCE" kind is used e.g. to generate the preference-list for the Firefox PDF Viewer, those options need to be carefully validated. With this patch we'll now check this unconditionally in development mode, during testing, and when creating the preferences in the gulpfile. --- gulpfile.mjs | 10 ++++-- test/unit/app_options_spec.js | 41 +++++++++++++++++++++++ test/unit/clitests.json | 1 + test/unit/jasmine-boot.js | 1 + web/app_options.js | 61 +++++++++++++++++++++-------------- web/preferences.js | 4 +-- 6 files changed, 90 insertions(+), 28 deletions(-) create mode 100644 test/unit/app_options_spec.js diff --git a/gulpfile.mjs b/gulpfile.mjs index 6d7033e40..cd56bbf15 100644 --- a/gulpfile.mjs +++ b/gulpfile.mjs @@ -863,11 +863,17 @@ async function parseDefaultPreferences(dir) { "./" + DEFAULT_PREFERENCES_DIR + dir + "app_options.mjs" ); - const browserPrefs = AppOptions.getAll(OptionKind.BROWSER); + const browserPrefs = AppOptions.getAll( + OptionKind.BROWSER, + /* defaultOnly = */ true + ); if (Object.keys(browserPrefs).length === 0) { throw new Error("No browser preferences found."); } - const prefs = AppOptions.getAll(OptionKind.PREFERENCE); + const prefs = AppOptions.getAll( + OptionKind.PREFERENCE, + /* defaultOnly = */ true + ); if (Object.keys(prefs).length === 0) { throw new Error("No default preferences found."); } diff --git a/test/unit/app_options_spec.js b/test/unit/app_options_spec.js new file mode 100644 index 000000000..13128404f --- /dev/null +++ b/test/unit/app_options_spec.js @@ -0,0 +1,41 @@ +/* Copyright 2024 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AppOptions, OptionKind } from "../../web/app_options.js"; +import { objectSize } from "../../src/shared/util.js"; + +describe("AppOptions", function () { + it("checks that getAll returns data, for every OptionKind", function () { + const KIND_NAMES = ["BROWSER", "VIEWER", "API", "WORKER", "PREFERENCE"]; + + for (const name of KIND_NAMES) { + const kind = OptionKind[name]; + expect(typeof kind).toEqual("number"); + + const options = AppOptions.getAll(kind); + expect(objectSize(options)).toBeGreaterThan(0); + } + }); + + it('checks that the number of "PREFERENCE" options does *not* exceed the maximum in mozilla-central', function () { + // If the following constant is updated then you *MUST* make the same change + // in mozilla-central as well to ensure that preference-fetching works; see + // https://searchfox.org/mozilla-central/source/toolkit/components/pdfjs/content/PdfStreamConverter.sys.mjs + const MAX_NUMBER_OF_PREFS = 50; + + const options = AppOptions.getAll(OptionKind.PREFERENCE); + expect(objectSize(options)).toBeLessThanOrEqual(MAX_NUMBER_OF_PREFS); + }); +}); diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 6ea14e1ed..4a43e773f 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -7,6 +7,7 @@ "annotation_spec.js", "annotation_storage_spec.js", "api_spec.js", + "app_options_spec.js", "bidi_spec.js", "cff_parser_spec.js", "cmap_spec.js", diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index 5852ae6da..da98ceeb3 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -50,6 +50,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/annotation_spec.js", "pdfjs-test/unit/annotation_storage_spec.js", "pdfjs-test/unit/api_spec.js", + "pdfjs-test/unit/app_options_spec.js", "pdfjs-test/unit/bidi_spec.js", "pdfjs-test/unit/cff_parser_spec.js", "pdfjs-test/unit/cmap_spec.js", diff --git a/web/app_options.js b/web/app_options.js index 998a6ed2c..9d49117ec 100644 --- a/web/app_options.js +++ b/web/app_options.js @@ -404,6 +404,35 @@ if (typeof PDFJSDev === "undefined" || PDFJSDev.test("GENERIC")) { const userOptions = Object.create(null); +if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING || LIB")) { + // Ensure that the `defaultOptions` are correctly specified. + for (const name in defaultOptions) { + const { value, kind } = defaultOptions[name]; + + if (kind & OptionKind.PREFERENCE) { + if (kind === OptionKind.PREFERENCE) { + throw new Error(`Cannot use only "PREFERENCE" kind: ${name}`); + } + if (kind & OptionKind.BROWSER) { + throw new Error(`Cannot mix "PREFERENCE" and "BROWSER" kind: ${name}`); + } + if (compatibilityParams[name] !== undefined) { + throw new Error( + `Should not have compatibility-value for "PREFERENCE" kind: ${name}` + ); + } + // Only "simple" preference-values are allowed. + if ( + typeof value !== "boolean" && + typeof value !== "string" && + !Number.isInteger(value) + ) { + throw new Error(`Invalid value for "PREFERENCE" kind: ${name}`); + } + } + } +} + class AppOptions { constructor() { throw new Error("Cannot initialize AppOptions."); @@ -421,36 +450,20 @@ class AppOptions { return undefined; } - static getAll(kind = null) { + static getAll(kind = null, defaultOnly = false) { const options = Object.create(null); for (const name in defaultOptions) { const defaultOption = defaultOptions[name]; - if (kind) { - if (!(kind & defaultOption.kind)) { - continue; - } - if ( - (typeof PDFJSDev === "undefined" || PDFJSDev.test("LIB")) && - kind === OptionKind.PREFERENCE - ) { - if (defaultOption.kind & OptionKind.BROWSER) { - throw new Error(`Invalid kind for preference: ${name}`); - } - const value = defaultOption.value, - valueType = typeof value; - if ( - valueType === "boolean" || - valueType === "string" || - (valueType === "number" && Number.isInteger(value)) - ) { - options[name] = value; - continue; - } - throw new Error(`Invalid type for preference: ${name}`); - } + if (kind && !(kind & defaultOption.kind)) { + continue; + } + if (defaultOnly) { + options[name] = defaultOption.value; + continue; } const userOption = userOptions[name]; + options[name] = userOption !== undefined ? userOption diff --git a/web/preferences.js b/web/preferences.js index b669f3742..312176fa8 100644 --- a/web/preferences.js +++ b/web/preferences.js @@ -23,7 +23,7 @@ import { AppOptions, OptionKind } from "./app_options.js"; class BasePreferences { #defaults = Object.freeze( typeof PDFJSDev === "undefined" - ? AppOptions.getAll(OptionKind.PREFERENCE) + ? AppOptions.getAll(OptionKind.PREFERENCE, /* defaultOnly = */ true) : PDFJSDev.eval("DEFAULT_PREFERENCES") ); @@ -48,7 +48,7 @@ class BasePreferences { ({ browserPrefs, prefs }) => { const BROWSER_PREFS = typeof PDFJSDev === "undefined" - ? AppOptions.getAll(OptionKind.BROWSER) + ? AppOptions.getAll(OptionKind.BROWSER, /* defaultOnly = */ true) : PDFJSDev.eval("BROWSER_PREFERENCES"); const options = Object.create(null);