前回、GDI+を活用した*jpg等イメージファイルを纏めて管理するアルバムソフトの開発に起因するバグ騒動について書きました。
【バグは続くよ、何処までも】アルバムソフト-バグとの闘い
今回はその後日譚です。
前回言及したバグは、
(1)「Microsoft Windows 10が、ファイル名に禁止している『\ / : * ? " < >』の禁則文字が、ファイルのプロパティダイアログの「ファイル作成日時」をコピーすると違法に(禁止されているものが、何ら警告もなく、容認されて)ファイル名に混入」する現象、と
(2)FileListダイアログを使った際にフルパスファイル名から正しくファイル名のみを抽出できない現象
についてでした。
以下にその後の対応と結果について記します。
1.ファイル名禁則文字混入事件
最初にこの問題に気が付いた自作料理の写真については、全てマニュアル(注)で禁則文字を削除し、再度アルバムソフトを使うと正常に扱えることが確認されました。
注:具体的な手順としては、Explorerの右クリックによるポップアップメニューの「名前の変更」を使って、表示されていない '?' (ASCII 0x3F) を選択-見えない文字ですが、ちゃんと選択できます-して削除キーを押しました。
次に同様の現象がjpgファイルのみならず、その保存フォールダー名にも表れている例(以下「軽井沢写真」)で、GDI+のImageクラスオブジェクトのインスタンスをファイル名コンストラクターで生成する際に、エラー表示なしにプログラムが落ちることを確認しました。
現象が特定され、再現実験で原因と責任の所在がMicrosoft Windows 10にあることが確認できたので、当方の原因対処は行わず、再発防止措置として「混入している'?'」を駆除するツールを作ることとしました。
本日このツール(禁則文字が混入して汚染しているので"FileNameCleaner"となずけました)を作成し、実行試験にかけましたが、
「パス、ファイル名が違法状態だとWIN32APIが正常に動作せず、プログラムによる対処は不可能」
という結論に至りました。具体的には、
(1)特定のフォールダーを選択し、その名称に'?' 等(他の禁則文字も含めました)があれば、それを除いたファイル名に書き換える。
(2)次にその「浄化した」フォールダー内の全ファイルに同様の措置を実施する。
というツールです。
試験で実行すると、
このようにフォールダー名を表示して6つの'?' があることを教えてくれます。しかし、「はい」を押すと、
フォールダー(ディレクトリー)の名称変更に失敗します。名称変更は最初フォールダー、ファイル共に適用できるWin32APIのMoveFile関数を使い、次にC++のランタイムのrename関数も使いましたが、いずれも討ち死にでした。
何故か?それは変更対象ソースのパス、ファイル名が違法なので、関数が受け付けてくれないからです。
簡単な関数では対応できないということで、WEBで色々と調べましたが妙策はなく、「新しくフォールダーを作る、①すべてのファイルをコピーする、②違法な最初のフォールダーとファイルを削除する」という面倒な方法も、矢張り①、②で違法パス、ファイル名を使わなければならないので関数はエラーとなる、という悲しい現実に直面しました。
因みにフォールダーを手作業で修復し、このツールを実行継続すると、ファイルで失敗する以前の問題としてフォールダー内のファイル検出関数(FindFirstFile、FindNextFile)の引数であるパスに問題があるために抽出に失敗します。
結果こういうことになりました。(プロパティダイアログからコピーした日付の'?'にご注目あれ。)
しかし、OS自体はこの'?'があっても正常に処理をしているのに、そのAPIはエラーを出すってどうなっているのか?と思います。日付の数字をあるコードで囲み、それが最終的に'?'に変換されて残ってしまうのでしょうか?'?'があっても処理ができるという裏関数があるなら、公開してほしかったです。いずれにしても、
ファイルを読み込んで即落ちする現象があったら、プロパティダイアログ等OSからコピーしてきたものであっても、禁則文字混入コンタミの可能性がある
事を疑ってください。
2.ファイル名首切り「ソ」問題事件
こちらは私に非がある問題ですが、内容は簡単で既に恒久的対処が終わっています。
冒頭↑の写真の通り、FileListダイアログを開くとイメージファイルから切り出された「日付から始まる料理のファイル名」が並ぶはずだったのですが、一部のファイルが「ーメンン」とか「ース」とかから始まっています。松田優作ではないですが、
「なんじゃぁ、こりゃ???」
となりました。(BCCSkeltonを使って20年、このような問題はなかったので。)
しかし、直ぐにこれが全角カタカナの「ソ」(2バイトコード)が悪さをしていること、恐らく第2バイトを'\'と間違えて誤処理をしているであろうことが推測され、即検証に入りました。
やはり、「ソーメン」や「ソース」の「ソ」(Shift-JISコードの0x835C)はバイトで見ると第2バイトが''\'(0x5C)'と同一です。
この為、FileListのGetFileName関数が効率性の為に文字列のお尻からチェックしていたため(注1)、「ソ」の第2バイト0x5Cを'\'と誤認して、例えば次の例では
例:"C:\Users\ysama\ピクチャ\料理\2019年8月7日ソーメンと厚揚げ、水菜チキンサラダ.jpg"
0x83 0x5C('\') ←cp(チェックポインター)
"ーメンと厚揚げ、水菜チキンサラダ.jpg"をファイル名と認識してしまいます。
それでは、ポインターが'\'を指した際に、その一つ前のバイトが「Shift-JISで使う第1バイト(0x81-0x9Fまたは0xE0-0xEF)」なら無視して進める、というアルゴリズムで書き直せばよいのでは、と思いました。が、「ソ」は思惑通り通過しましたが、「料理」の「理\(0x97 0x9D 0x5C)」と「ピクチャ」の「ャ\(0x83 0x83 0x5C)」が「絶妙」にヒットして「Shift-JISコード」とみなされ、今度は
0x83 0x83 0x5C
例:"C:\Users\ysama\ピクチャ\料理\2019年8月7日ソーメンと厚揚げ、水菜チキンサラダ.jpg"
0x97 0x9D 0x5C 0x83 0x5C('\') ←cp(チェックポインター)
ファイル名を"ャ\料理\2019年8月7日ソーメンと厚揚げ、水菜チキンサラダ.jpg"と認識してしまいました。
ということで、「お尻から見てゆくアルゴリズムには限界があり、解決しない」と、正統派の頭から見てゆくアルゴリズムに変更しました。しかし、結果は"ャ\料理\2019年8月7日ソーメンと厚揚げ、水菜チキンサラダ.jpg"と変わりありません。
最初「何故????」と当惑しましたが、「Shift-JISで使う第1バイト(0x81-0x9Fまたは0xE0-0xEF)」がcharだと負数になる為、条件式が正しく機能していないことが分かり、「char → unsigned char」へ変更し、C++11のBCC102で耐えられるようにキャスト変換等調整し、最終形としました。(注2)
注1:問題のある旧アルゴリズム
////////////////////////////////////
//ユーザー定義関数
//フルパス名からファイル名だけを取得
////////////////////////////////////
char* CFILELIST::GetFileName(char* pathname) {
static char fn[MAX_PATH]; //データをスタックに置かない為
lstrcpy(fn, pathname); //引数の文字列のコピーを作る
//cpをpassnameの終端(NULL)のひとつ前に進める
char* cp = fn + lstrlen(fn) - 1;
if(*cp == '\"' && *fn == '\"')
*cp = NULL; //「""」で囲まれていれば外す
//文字列の後ろから最初の'\'を(保険でfnの先頭迄)探す
while(*cp != '\\' && cp > fn) {
--cp;
}
//*cpが「\」、「"」(fn)の何れでも一つ進める("ファイル名"の場合はそのまま)
if(*cp == '\\' || *cp == '\"')
cp++;
return cp; //ファイル名の先頭アドレスを返す
}
注2:前からチェックして行き、Shift-JISコードはスキップするように変更
////////////////////////////////////
//ユーザー定義関数
//フルパス名からファイル名だけを取得
////////////////////////////////////
char* CFILELIST::GetFileName(char* pathname) {
static char fn[MAX_PATH]; //データをスタックに置かない為
lstrcpy(fn, pathname); //引数の文字列のコピーを作る
//cpをpassnameの終端(NULL)のひとつ前に進める
unsigned char* sp = (unsigned char*)fn;
unsigned char* ep = (unsigned char*)fn + lstrlen(fn) - 1;
char* last = 0; //最後の'\'を指すポインター
if(*sp == '\"' && *ep == '\"')
*ep = NULL; //「""」で囲まれていれば外す(NULLを指す)
//文字列の'\'を探す
while(sp < ep) {
if(*sp == '\\')
last = (char*)sp; //spを進めるのでlastに記録
//Shift-JISの第1バイト(0x81-0x9Fまたは0xE0-0xEF)なら2バイト進める
if((*sp >= 0x81 && *sp <= 0x9F) || (*sp >= 0xE0 && *sp <= 0xEF))
sp += 2;
else
sp++; //spを進める
}
return ++last; //ファイル名の先頭アドレスを返す
}
また、これを教訓にBCCSkeltonのCARGクラスも「先頭からチェック」アルゴリズムに変更しました。(ファイル日付:2022年3月20日のものが修正版)CARGクラスを使っている旧いプログラムでShift-JISのダメ文字問題があれば、再コンパイルをすれば解決できます。
なお、この修正版は問題となった文字列を使ってテストし、正常に動作したことを報告させていただきます。
<テストプログラム>
CARG arg("C:\\Users\\ysama\\ピクチャ\\料理\\2019年8月7日ソーメンと厚揚げ、水菜チキンサラダ.jpg");
MessageBox(m_hWnd, arg.Drive(), "Drive", MB_OK); //"C:"と表示
MessageBox(m_hWnd, arg.Path(), "Path", MB_OK); //"C:\\Users\\ysama\\ピクチャ\\料理"と表示
MessageBox(m_hWnd, arg.FileName(), "FileName", MB_OK); //"2019年8月7日ソーメンと厚揚げ、水菜チキンサラダ.jpg"と表示
MessageBox(m_hWnd, arg.Ext(), "Ext", MB_OK); //"jpg"と表示
以上を持ちまして、今回のバグ騒動のピリオッドとさせていただきます。





