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?

Unicode正規化でファイル名が衝突する問題を、実データ537,689件で調べた

0
Last updated at Posted at 2026-08-30

最初は別のことを調べていた。

「Macで作った日本語のファイル名は、Windowsに持ってくると濁点が分解して壊れる」

よく聞く話だし、実際に起きる。検査ツールを書いて、全パターンのテストも通した。
そこまでやってから、実データで何件出るのかを数えていないことに気づいた。

数えた。0件だった。

だからその看板を捨てた。かわりに、数えている途中で別のものが出てきた。この記事はその記録。

先に、何をどう数えたか

数える前に単位を決めておかないと、あとで足せなくなる。実際に一度間違えた。

母集団の単位   走査した「名前」の数。ファイル名とフォルダ名。
               同じ名前が別のフォルダにあれば、別の1件として数える。

検出の単位     行数と組数を分ける。
               衝突は2つのファイルの間で起きるので、ツールは片方ずつ2行出す。
               「26行」は「13組」。ここを混ぜると倍になる。

最初に集計したとき、片方を組数、もう片方を行数で数えて足していた。数が合わなくなって気づいた。

母集団

広く使われているOSSリポジトリ 10本      385,707 名前
npmのパッケージ 891本(配布物の中の名前)  151,982 名前
────────────────────────────────────────────
合計                                    537,689 名前

OSSはgit clone --depth 1したもの。npmはtarballの中に記録されている名前をそのまま読んだ。
展開していない。展開すると、展開した側のファイルシステムの都合で名前が変わってしまう可能性があるから。

結果

                          名前       検出行   衝突の組
OSSリポジトリ 10本      385,707        26        13
npm 891パッケージ       151,982       186        62
────────────────────────────────────────────────
合計                    537,689       212        75

内訳(組数)  大小文字の衝突 13 / 正規化による衝突 62
              ※正規化による62組は、すべてCJK互換漢字によるもの

濁点分解 0件。最初に調べようとしていたものは、1件も出なかった。

出現率は0.014%。1万件に約1.4件だった。
少なくとも今回調べたデータでは、頻繁に起きる問題とは言えない。

出てきたもの その1: 大文字小文字

OSSの26行、13組は、全部ひとつのリポジトリに入っていた。Linuxカーネルのソースだ。

xt_MARK.h      と  xt_mark.h
xt_CONNMARK.h  と  xt_connmark.h
xt_DSCP.h      と  xt_dscp.h
xt_TCPMSS.h    と  xt_tcpmss.h

これは古くから知られている。Linux系のファイルシステムでは大文字と小文字を区別するため、
両方が並んで存在できる。WindowsのNTFSとmacOSの既定は区別しないので、同じ名前として扱われる。

残りの9本のリポジトリは0件だった。

出てきたもの その2: CJK互換漢字

npmのほうは、891パッケージのうちたった1本にだけ集中していた。日本語のデータを配布しているパッケージ。

62組ある。中身はこう。

侮.json = U+FA30 . j s o n     ← CJK COMPATIBILITY IDEOGRAPH-FA30
侮.json = U+4FAE . j s o n     ← 普通の漢字

見た目は完全に同じ。同じフォルダに両方が入っている。

CJK互換漢字というのは、互換性のために別の符号位置を与えられた漢字だ。
名前に「互換」と付くので互換等価かと思うところだが、ほとんどは正準等価になっている。
だからNFCを通すと、U+FA30はU+4FAEになる。62組が、正規化した瞬間に同じ名前になる。

ここは大事なので数えた。

CJK互換漢字のブロック  割り当て済み 1,014 文字
  正準分解を持つ        1,002
  互換分解を持つ            0
  分解を持たない           12

互換分解を持つものは1つも無い。全角のA(U+FF21)と比べると違いがはっきりする。

U+FA30 侮   分解 4FAE          → NFC で変わる
U+FF21 A   分解 <wide> 0041   → NFC では変わらない。NFKC でだけ変わる

互換等価ならNFCでは何も起きないので、この62組は衝突しない。
衝突するのは正準等価だからだ。

このパッケージ名は書かない。悪いのはこのパッケージではなくて、
「同じフォルダに入れてはいけない組み合わせだと誰も教えてくれない」ことのほうだから。

そして濁点分解は0件だったのに、これは62組あった。
今回の母集団で観測できたのは、濁点ではなくこちらだった。

「1万件に1.4件」は、買う側から見た数字ではない

この率はファイル名を分母にしている。でも人が検査したいのは「1個の名前」ではなくて、
「この納品物ぜんぶ」「このリポジトリぜんぶ」という単位のはずだ。

そこで単位を変えて数え直した。npmのパッケージを2,467本に増やして、
1パッケージにつき1件でも出るかを見た。

単位の大きさ(名前の数)      単位数   当たり   当たり率
0 〜 99                      2,137       0     0.00%
100 〜 499                     210       0     0.00%
500 〜 1,999                    80       0     0.00%
2,000 〜 9,999                  34       2     5.88%
10,000 以上                      6       0     0.00%
────────────────────────────────────────────
合計                         2,467       2     0.08%

名前が2,000未満の単位は2,427本あって、当たりは0。
当たった2本は、どちらも名前が数千ある大きなパッケージだった。

OSSリポジトリのほうも同じ形をしている。10本中で当たったのはLinuxカーネルの1本だけで、
それは10万を超える名前を持っていた。残り9本は5,118から67,214名前まであって、全部0だった。

今回の2,467パッケージでは、名前が2,000未満の単位から1件も出なかった。

自分でツールを作った側として、これは書きたくない結果だった。でも測ったらこうだった。

ここで「消える」とは書かない

書きたくなるところだが、実測で確認したのは「同じ名前として扱われる」までだ。

実際にファイルが消えるかどうかは、展開する道具の挙動で決まる。
上書きするものもあれば、エラーで止まるものも、別名を付けるものもある。そこは測っていない。

自分で作ったテストデータで、NFCに正規化しながらコピーして1件が消えるのは確認した。
それは自分で作った1件であって、実データで観測した数ではない。分けて書く。

おまけ: NTFSの大文字変換表はUnicodeと違う

大文字小文字の衝突を判定するのに、最初はPythonのstr.upper()で近似していた。
近似のままにしておくのが気持ち悪かったので、実機で答え合わせした。

Unicodeの表で「1文字が1文字に写る」ペアを全部集めると1,422組ある。
その1,422組それぞれについて、Windows上でファイルを2つ作って、1つになるかどうかを見た。

測った組    1,422
一致          946
予測が過剰    476   (衝突すると言ったのに、NTFSはしなかった)
予測が不足      0   ← 見逃しは1件も無い
一致率      66.526%

3分の1が外れた。ただし、「予測が不足」が0なのが大事で、外れ方は全部「過剰に鳴らす」側だった。
検査ツールとしては、見逃すより鳴らしすぎるほうがまだいい。

ズレの中身を見ると、はっきりした構造があった。

BMPの外(U+10000以上)  260組が全部ズレ
  → NTFSの$UpCaseはUTF-16の符号単位で引く16bitの表なので、代理対を畳めない。これは規則
BMPの中                  216組
  → 特定の文字ブロックに固まっていた。例: U+00B5 µ→Μ / U+0131 ı→I

この476組を表にしてツールに読ませたら、同じ機械でもう一度測って一致率100.000%になった。

測った組    1,422
一致        1,422
予測が過剰      0
予測が不足      0
一致率     100.000%

この表は手で書いていない。測って作った。別のWindowsでは中身が違う可能性があるので、
測り直して作り直せるようにしてある。

測れていないこと

  • macOSのAPFS実機では確かめていない
  • Windowsは1台でしか測っていない。別のビルドで$UpCaseが違う可能性がある
  • 母集団はOSSとnpmだけ。日本の制作会社が納品するファイル群のような母集団には届いていない
  • 濁点分解が0件だったのは「起きない」からではなく、「起きる母集団に届いていない」からかもしれない。そこは区別がついていない

まとめ

名前を分母にすると   537,689件で75組     0.014%
単位を分母にすると   2,467本で2本        0.08%
  ただし名前が2,000未満の単位 2,427本では 当たり0

内訳
  大小文字の衝突      13組(Linuxカーネル)
  正規化による衝突    62組(CJK互換漢字・日本語のパッケージ1本)
  濁点分解             0件 ← 最初に探していたもの

この記事で一番書きたかったのは75という数字ではなくて、
探していたものが0件だったので、看板を下ろしたというほうだ。

「Mac→Windowsで濁点が壊れる」は、記事として売りやすい。実際に起きる現象でもある。
でも自分が届いた実データには1件も無かった。無かったものを「よくある問題です」とは書けない。

かわりに、探していなかったCJK互換漢字が62組出てきた。
少なくとも今回調べた実データでは、こちらのほうが具体的に観測できた。

こういう名前の衝突を検査するツールを書いている。
実データで何が起きるのかを、もう少し測れる形にしている。
検査ツールと測定結果はここで公開している。

違う数字が出たら、ぜひIssueで教えてほしい。
このリポジトリで一番ほしいのは、別の環境での実測結果だ。


測定日 2026年8月30日 / Unicode 15.0.0 / Python 3.12.3


測ってほしいことがあれば

手元で再現できるものなら、実際に動かして数字で返します。コメントに環境と再現手順を書いてもらえると早いです。結果がどちらに転んでも、そのまま出します。

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?