AI利用と関係の開示:本稿とJavaScriptの例は、Yan Wang / LensUp向けにCodexが生成しました。Codexが実行したローカル検査の範囲を明記します。人間のアカウント所有者が自ら実験・独立検証したという意味ではありません。PDFを書き出すコードや製品修正ではなく、並べ替え計画を検査する独立した例です。
ページを移動した後もサムネイルが四つある。それだけでは、元の四ページが一度ずつ残っていると判断できません。同じページを二回含め、別のページを落としても、配列の長さは四のままだからです。
ここでは、一つのPDFの全ページを残して順序だけ変える操作を対象にします。削除、複製、別ファイルの追加を含める場合は、この契約をそのまま使えません。
最初に検査するのはページ内容ではなく計画
例のインデックスは、元ファイルの物理ページをゼロから数えたものです。四ページなら有効な値は0から3までです。印刷されたページ番号やPDFのページラベルは入力に使いません。
function validateReorder(order, pageCount) {
if (!Number.isSafeInteger(pageCount) || pageCount < 1) {
throw new RangeError("pageCount must be a positive safe integer");
}
if (!Array.isArray(order) || order.length !== pageCount) {
throw new TypeError("order must contain one slot per source page");
}
const seen = new Set();
for (let i = 0; i < order.length; i += 1) {
const page = order[i];
if (!Number.isInteger(page) ||
page < 0 || page >= pageCount || seen.has(page)) {
throw new RangeError("order must be a permutation of source indexes");
}
seen.add(page);
}
return order.slice();
}
配列の長さ、各値の型と範囲、重複をまとめて検査します。通常の数値配列を想定した例であり、悪意のあるProxyやgetterを防ぐ境界ではありません。
添字で全スロットを読むのも意図的です。疎配列の穴は、長さには含まれても、配列メソッドによっては処理されません。このループでは穴を読むとundefinedになり、整数検査で失敗します。
通してはいけない反例を先に置く
次のブロックを同じESモジュールで関数の後に置けます。
import assert from "node:assert/strict";
assert.deepEqual(validateReorder([3, 0, 1, 2], 4), [3, 0, 1, 2]);
for (const order of [
[0, 1, 1, 3],
[0, 1, 2, 4],
[0, 1, 2, "3"],
[0, 1, , 3],
]) {
assert.throws(() => validateReorder(order, 4));
}
const sourcePlan = [1, 0];
const acceptedPlan = validateReorder(sourcePlan, 2);
sourcePlan.reverse();
assert.deepEqual(acceptedPlan, [1, 0]);
重複、範囲外、文字列、穴は、どれも「長さは四」という検査を通せます。ここで失敗させたいのは、そうした計画です。返す配列をコピーにしているため、元の配列を後から変更しても、受け取った計画は変わりません。
本稿のローカル検査は、これらに加え、不正なページ数、非配列、負数、小数、NaN、Infinity、短い配列も対象にしています。実行結果は原稿と一緒に保存しています。PDFの生成、ブラウザー操作、暗号化文書、フォーム互換性を試したという意味ではありません。
表示した二つのJavaScriptブロックは、macOSのNode.js v22.23.4で一つのESモジュールとして実行しました。再現するには、順にreorder-check.mjsへ保存し、node reorder-check.mjsを実行します。本文とは別の保存済みハーネスで、上記の追加反例も検査しています。
正しい順列でも、利用者の希望と違うことがある
[3, 0, 1, 2]も[0, 1, 2, 3]も、有効な順列です。表紙を先頭にしたい利用者にとって、後者が不正解であっても、この関数は拒否しません。
その判定には、利用者が決めた順序が必要です。「表紙、説明、申請用紙、添付資料」のように、ページ内容で期待値を残します。書き出し後のPDFを開き、その順序と比較します。元ページ数が同じという確認だけで終わらせません。
保存処理の入力と結果を混同しない
この関数はPDFのバイト列を読みません。書き出し処理が古い計画を使った、移動先を取り違えた、ページの内容を欠落させた、といった不具合は発見できません。
また、配列のコピーは文書全体のスナップショットではありません。保存中に元文書へページが追加される設計なら、計画と元文書の版を結び付ける必要があります。
保存後には実際のファイルを開き、ページ内容の順序を確かめます。リンクやフォームが必要なら、それぞれ別に検査します。デジタル署名付き原本は保存しておき、変更後の見た目から署名の検証結果を推測しません。
この例で分けたいのは、入力計画の構造と、保存されたファイルの結果です。前者の検査が通っても、後者の検査は残っています。