0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

NFC と表示名を分けて考える JPG 名の衝突テスト

0
Posted at

見た目が同じファイル名でも、文字列の中身が違うことがあります。café.jpg の「é」は、1文字でも、e と結合文字の組み合わせでも表せます。以下は、変換後の JPG ファイル名候補(パスを含まない名前)を受け取る教材用ポリシーです。製品の実装やデコーダーではありません。

比較には NFC 正規化後の小文字を使います。衝突しない候補はそのまま返し、衝突時は拡張子を除いた部分の綴りを保って -N.jpg を付けます。拡張子は小文字になるので、scan.JPG は scan-2.jpg になり得ます。-2 から試し、その候補も使用済みなら次の番号へ進みます。比較キーと表示名は別の役割です。

export function allocateJpgNames(names) {
  if (!Array.isArray(names)) throw new TypeError('names must be an array');
  const taken = new Set();
  const key = name => name.normalize('NFC').toLowerCase();
  return names.map(name => {
    if (typeof name !== 'string' || !/\.jpg$/i.test(name) ||
        name.length <= 4 || /[\\/]/.test(name) || name.includes(String.fromCharCode(0))) {
      throw new TypeError('expected a JPG leaf candidate');
    }
    const stem = name.slice(0, -4);
    let candidate = name;
    let suffix = 2;
    while (taken.has(key(candidate))) candidate = `${stem}-${suffix++}.jpg`;
    taken.add(key(candidate));
    return candidate;
  });
}

次のブロックを同じ .mjs ファイルの関数の後ろに続けて置き、Node で実行します。関数が同じファイルにあるため、元のテストから関数の import だけを省いています。

import assert from 'node:assert/strict';
const cases = [
 [['scan.jpg','scan.jpg'],['scan.jpg','scan-2.jpg']],
 [['Scan.jpg','scan.JPG'],['Scan.jpg','scan-2.jpg']],
 [['caf\u00e9.jpg','cafe\u0301.jpg'],['caf\u00e9.jpg','cafe\u0301-2.jpg']],
 [['scan.jpg','scan-2.jpg','scan.jpg'],['scan.jpg','scan-2.jpg','scan-3.jpg']],
 [[],[]]
];
for(const [input,expected] of cases) {
 const saved=[...input];assert.deepEqual(allocateJpgNames(input),expected);assert.deepEqual(input,saved);
}
assert.deepEqual(allocateJpgNames(['scan.jpg','scan.jpg']),allocateJpgNames(['scan.jpg','scan.jpg']));
for(const invalid of [null,'a.jpg',[null],[''],['.jpg'],['a.png'],['dir/a.jpg'],['dir\\a.jpg'],['a\0.jpg']]) assert.throws(()=>allocateJpgNames(invalid));
console.log(JSON.stringify({valid_cases:cases.length,fresh_run_repeat:1,reject_cases:9,boundary:'synthetic candidate names only; no image/ZIP/filesystem operation'}));

5件の有効な候補リストには空配列も含まれ、どのケースでも入力配列が変更されないことを確かめます。別呼び出しの再現確認は1件、不正入力は9件です。2026年10月31日の保存済み実行は Node v22.23.4 で、終了コード0でした。失敗するスタブでの確認は終了コード1です。

NFC は正準等価を扱うもので、互換等価まで扱う NFKC とは異なります。toLowerCase() も完全な Unicode case folding ではありません。MDN の normalize 解説で正規化形式を確認できます。

接尾辞は元画像の ID ではなく、呼び出しごとに割り当てられるものです。元画像 ID・ページ・出力名の対応は別に記録してください。この入力検査は安全で移植可能なファイル名を保証するものではなく、各ファイルシステムでの比較や展開も検証していません。

開示:GPT-6.1 Terra を使って Yan Wang / LensUp のために執筆しました。Yan Wang は LensUp の関係者です。本文は独立した編集AIが推敲し、Codexが事実確認(コードを含む独立レビュー)を行いました。人間の編集者やオーナーによる確認・テストは行っていません。根拠は2026年10月31日のソース・公開ページの確認と保存済みの合成 Node テストです。Node テストは合成した JPG ファイル名候補のみが対象で、実際の画像変換、TIFF デコード、ZIP 作成、ファイルシステム、端末でのテストは行っていません。内訳は有効な候補リスト5件(空配列を含む)、別呼び出しの再現確認1件、不正入力9件で、Node v22.23.4 で終了コード0でした。製品関数との一致を検証するテストでもありません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?