前回の
== 引用 ==
・
・
・
以上ですが、
(ア)ファイルデータはサクラエディターで作成、DumpでNULL終端付きで保存されていることを確認済。
(イ)Buffは毎回動的にメモリーを確保しているし、ファイルデータ(文字列)の最後にはNULL終端が付いている。
(ウ)にもかかわらず、↑の現象が発生することを考えると、
(i) ファイルデータがBuffに読み込まれた段階でごみ問題が発生しており、
(ii) Buffが同じアドレスで生成され、ReadFile関数でファイルデータのNULL終端が読み込まれない。
と考えるしかないのではないでしょうか?
分からない...
== 引用 ==
から、悩みに悩みましたが、結局実験と観察を繰り返してやっとこさ解決しました。
まず、
1.「(ア)ファイルデータはサクラエディターで作成、DumpでNULL終端付きで保存されていることを確認済。」
→これは間違いでした。Dumpで文字列配列[ファイルサイズ]がNULLであったので誤解しました。本来は文字列配列[ファイルサイズ - 1]が最終文字であり、それは(文字列の最後を確認する為につけた)’X’でした。要すれば「ファイルからNULL終端無しの文字列をバッファに読み込んだ」為に文字列の後にゴミが付くことになったわけです。(注)
注:GetFileSize関数の「ファイルサイズ」が具体的に何を意味するのか、はMicrosoft Docにも書かれていませんが、「書き込まれたバイト数」だと思っていました。(詳しく調べようとして、Windows NTFSや、そのEOF関連を調べると頭が痛くなります。)文字列データの書き込みでは常にNULL終端を入れてセーブするものと考え、ファイルサイズもNULL終端のバイト数まで含むものと理解していましたが、そういえば、それを確認したことはありませんでしたね。
次に、
2.「(イ)Buffは毎回動的にメモリーを確保しているし、ファイルデータ(文字列)の最後にはNULL終端が付いている。」
→これはその通りであり、「LPBYTE Buff = new BYTE[dwFileSize]();」の"()"を付けることでゼロ初期化されるのですが、これが正しくなかった(NULL終端が無い)ので、NULL終端があるという前提のMultiByteToWideChar関数の処理が、確保したBuff配列以降のメモリー迄読んでしまい、ゴミも変換されてしまったということです。これはUTF-8のみならず、WCHARでもNULL終端を付ける為に「LPBYTE Buff = new BYTE[dwFileSize + 2]();」として読込バッファを確保すれば解決することになる筈です。
余談ですが、
3.「(ii) Buffが同じアドレスで生成され、ReadFile関数でファイルデータのNULL終端が読み込まれない。」
→如何に「動的にメモリーを確保」しても、それは「前に開放したメモリー領域ではない」ということと等価ではないので、「同じ場所にプログラムとOSがメモリーを割り当てる」ことは別に不思議ではなく(注)、「長 ー い 文 章 の サ ン プ ル」を読み込んだ後、(NULL終端のない)「短い文章のサンプル」を読み込むと、解放した「長 ー い 文 章 の サ ン プ ル」の場所に「短い文章のサンプル」の長さだけゼロ初期化された結果、バッファには「 の サ ン プ ル」というデータが並び、この下線部にファイルデータが上書きされた、というのが今回の現象の機序でした。
注:実際、実験を行い、バッファーとそれ以降のメモリーを確認して、解放されたメモリー領域にまたメモリーが確保されることも確認しています。
ということで、上記の知見に基づくコード修正を行い、ついでなのでShift-JIS対応も行って最終コードを以下に紹介します。(実験用のトラップのコード込みです。)
<最終的なコード>
//ファイルを読み込む
BOOL bSuccess = FALSE;
HANDLE hFile = CreateFileW(FileName, GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, NULL, NULL); //ファイルを読込みでオープン
if(hFile != INVALID_HANDLE_VALUE) { //オープンできたか
DWORD dwFileSize = GetFileSize(hFile, NULL);
if(dwFileSize != 0xFFFFFFFF) { //-1は(0xFFFFFFFF)エラー
LPBYTE Buff = new BYTE[dwFileSize + 2](); //読み込みバッファ(NULL終端分追加)用ポインター
//"new 配列[]()"でゼロ初期化し、WCHARファイルにも対応できるよう最後の2バイトをNULLにしている
MessageBoxA(0, (char*)Buff, "Buffの初期状態", MB_OK);
DWORD dwRead;
if(ReadFile(hFile, Buff, dwFileSize, &dwRead, NULL)) {
char str[256];
wsprintfA(str, "File size = %d and bytes read = %d", dwFileSize, dwRead);
MessageBoxA(0, str, "File size in byte", MB_OK);
MessageBoxA(0, (char*)Buff, "Buff in Ascii", MB_OK);
bSuccess = TRUE; //読み込み成功
//BOM付UTF-16、UTF-32の場合
if(Buff[0] == 0xFF && Buff[1] == 0xFE) {
if(Buff[2] | Buff[3]) { //いずれもNULLではない→UTF-16のBOM
delete [] m_str; //元の文字列を廃棄
//BOM以降のデータをm_strに収納する
m_str = new WCHAR[dwFileSize / sizeof(WCHAR)];
//正確には"new WCHAR[(dwFileSize + 2 <NULL> - 2 <BOM>) / sizeof(WCHAR)]
CopyMemory(m_str, Buff + 2, dwFileSize - 2); //BOMを除外
}
else { //Buff[2]とBuff[3])がいずれもNULL(UTF-32またはUTF-16でNULLのみ)
bSuccess = FALSE; //BOM付UTF-32の場合、読み込み失敗扱い
}
}
//BOM付UTF-8の場合
else if(Buff[0] == 0xEF && Buff[1] == 0xBB && Buff[2] == 0xBF) {
dwRead = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)(Buff + 3), -1, NULL, NULL);
if(dwRead) { //UTF-8文字列の場合
//m_str用メモリーの初期化
delete [] m_str;
m_str = new WCHAR[dwRead];
//UTF8をWCHARへ変換
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)(Buff + 3), -1, m_str, dwRead);
}
else { //BOM付UTF-8で変換不能の場合
bSuccess = FALSE; //何もせず読み込み失敗扱い
}
}
//UTF-16、32、8、何れのBOMもない場合
else {
//先ずBOM無しUTF-8か否かをチェック
if(dwRead = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)Buff, -1, NULL, NULL)) {
//m_str用メモリーの初期化
delete [] m_str;
m_str = new WCHAR[dwRead];
//UTF8をWCHARへ変換
MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, (LPCCH)Buff, -1, m_str, dwRead);
}
//次にSift JIS(コードページCP_ACP)か否かをチェック
else if(dwRead = MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, (LPCCH)Buff, -1, NULL, NULL)) {
//m_str用メモリーの初期化
delete [] m_str;
m_str = new WCHAR[dwRead];
//SJISをWCHARへ変換
MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS, (LPCCH)Buff, -1, m_str, dwRead);
}
//BOMが無く、UTF-8またはSJISではない場合、読み込み失敗扱い←「丸呑み」はやめました
else {
bSuccess = FALSE;
}
}
}
delete [] Buff; //読み込みバッファ解放
}
CloseHandle(hFile); //ファイルのクローズ
}
return bSuccess;
なお、ファイルの書き込みは特に問題は生じず(注)、
bool ToFile(WCHAR*, WCHAR*, HWND); //ダイアログを使ったUTF-16ファイルへの書き込み
bool ToFile(WCHAR*); //UTF-16ファイルへの書き込み
bool ToUTF8File(WCHAR*, bool); //UTF-8ファイルへの書き込み(boolはBOM有無のフラグ)
bool ToSJISFile(WCHAR*); //SJISファイルへの書き込み
という4種類の関数を用意しました。
注:実はWCEditorの本体のコーディングで誤解から、EDITコントロールの文字数が一つ少なくなるトラブルがあり、これも(昔は分かっていたのですが、すっかり忘れてしまい)誤解から生じていたことが分かりました。皆様もご注意ください。
WM_GETTEXTLENGTH→戻り値はテキストの文字数です。この文字数には終端ヌル文字は含まれません。
WM_GETTEXT→wParamには取得するテキストの文字数を入れますが文字数には終端ヌル文字も含まれます。(WM_GETTEXTLENGTHで取得した値に+1しなければならない)また、戻り値はコピーされたテキストの文字数が返ります。この文字数には終端ヌル文字は含まれません。
なお、サクラエディタ―でつくったサンプルとWCEditorで保存したファイルを比較するとUTF-16で2バイト、UTF-8やSJISで1バイトファイルサイズが異なります。これはWCEditorがNULL終端迄保存しているため(↓)ですが、これを止めるか否か考え中です。(今回CSTRはサクラエディターと同じようにNULL終端無しにセーブされたデータをNULL終端を付けて読み込む形にしたので、NULL終端無しに保存する方が合理的です。しかし、今回のような問題からもNULL終端を付けたデータの方がより安全であるということも言えます。NULL終端を付けたファイルを読み込んで、更に末尾にNULL終端が追加されても、再度書き込む際にはNULL終端は一つだけつけるので、ファイルサイズが膨張してゆくことはありません。)
<UTF-16>
//バッファから文字列長取得し、NULL終端も加算
DWORD dwTextLength = (lstrlenW(m_str) + 1) * sizeof(WCHAR);
<UTF-8>
//WCHARをUTF8へ変換した際の文字列長(バイト-NULL終端を含む)を取得
DWORD dwBuffSize = WideCharToMultiByte(CP_UTF8, WC_ERR_INVALID_CHARS, m_str, -1, NULL, NULL, NULL, NULL);
<SJIS>
//WCHARをSJISへ変換した際の文字列長(バイト-NULL終端を含む)を取得
DWORD dwBuffSize = WideCharToMultiByte(CP_ACP, WC_ERR_INVALID_CHARS, m_str, -1, NULL, NULL, NULL, NULL);
いずれにしても、これでECCSkeltonのCSTRクラスは一応の完結を迎えたと考えて良いでしょう。
しっかし、今回のバグ取りは疲れました!