Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ファイルダイアログとフォルダ選択ダイアログでのバッファオーバーランを修正 #1599

Merged

Conversation

usagisita
Copy link
Contributor

@usagisita usagisita commented Mar 21, 2021

PR の目的

ファイルダイアログ、フォルダダイアログへ長い文字列を指定するとバッファオーバーランする不具合の修正をします。

カテゴリ

  • 不具合修正
  • リファクタリング

PR の背景

極限値までいかなくても、ちょっと拡張子リストが長かったりすると、おかしいようなので、対策をします。

PR のメリット

バグが減ります。

PR のデメリット (トレードオフとかあれば)

ほとんどないと思います。

自動テストがないので、コードレビューが面倒です。

仕様・動作説明

「名前を付けて保存」ダイアログで、ファイルの種類の拡張子リストが正常に表示されないまたはクラッシュするのが修正されます。
保存ダイアログでは「ファイルの種類」で「ユーザ指定(*.txt:*.type;*.*)」と「テキストファイル(*.*)」と「すべてのファイル(*.*)」が選べます。
そのユーザ指定には今、開いているファイルの拡張子と現在のタイプ別設定が「ユーザ指定(*.open_file_ext:*.txt;*.log;*.*)」のように一度に表示されますが、長さ制限が足りていません。
開いているファイルの拡張子と、タイプ別設定に登録された拡張子リストが合わせてかなり長い場合、異常終了していました。

また同様に以下のマクロを実行する時も異常終了していました。
以前は落ちていたので実質的な影響はないと思います。
Editor.FileOpenDialog / FileSaveDialog("file_name")の指定にMAX_PATH制限を導入します。
Editor.FileOpenDialog("file_name.txt", "*.ext_filter")ext_filterが190文字前後以上長いとバッファオーバーランしてどこかおかしな動作をするのが修正されます。
Editor.FolderDialog("説明","default_folder")default_folderMAX_PATH制限を導入します。

PR の影響範囲

元の制限は
CFileExt::FileExtInfoTag::m_szExt[MAX_TYPES_EXTS*3+1]
MAX_TYPES_EXTSは64なので193文字(NULL含む)まで入力可能でした。
それ以上はチェックされずにオーバーランします。
こちらは保持する配列ごとstd::vector'とstd::wstringによりリファクタリングしました。

wcscpy( m_puFileExtInfo[m_nCount].m_szExt, pszExt );

CDlgOpenFile_CommonFileDialogCDlgOpenFile_CommonItemDialogm_szDefaultWildCardSFilePathなのでMAX_PATH制限があります。
こちらも異常終了する原因です。
こちらもstd::wstringに変更しました。

wcscpy( m_szDefaultWildCard, pszUserWildCard );

wcscpy( m_szDefaultWildCard, pszUserWildCard );

呼び出し側の関連処理では
CDocFileOperation.cppCDocFileOperation::SaveFileDialog()では_MAX_PATH+10文字の制限がありました。

WCHAR szDefaultWildCard[_MAX_PATH + 10]; // ユーザー指定拡張子
{
LPCWSTR szExt;
const STypeConfig& type = m_pcDocRef->m_cDocType.GetDocumentAttribute();
//ファイルパスが無い場合は *.txt とする
if(!this->m_pcDocRef->m_cDocFile.GetFilePathClass().IsValidPath()){
szExt = L"";
}
else {
szExt = this->m_pcDocRef->m_cDocFile.GetFilePathClass().GetExt();
}
if (type.m_nIdx == 0) {
// 基本
if (szExt[0] == L'\0') {
// ファイルパスが無いまたは拡張子なし
wcscpy(szDefaultWildCard, L"*.txt");
}
else {
// 拡張子あり
wcscpy(szDefaultWildCard, L"*");
wcscat(szDefaultWildCard, szExt);
}
}
else {
szDefaultWildCard[0] = L'\0';
CDocTypeManager::ConvertTypesExtToDlgExt(type.m_szTypeExts, szExt, szDefaultWildCard);

ConvertTypesExtToDlgExt(type.m_szTypeExts, szExt, szDefaultWildCard)
szExtにファイルの拡張子を指定して、タイプ別設定と一緒にデフォルトフィルターに指定しています。
ローカル変数szDefaultWildCardに必要とされる最大値はMAX_PATH+MAX_TYPES_EXTS*2前後で足りていません。
m_szTypeExtsは「a;b;c」「.a;.b;.c」→「*.a;*.b;*.c」のように展開されるので最大でも約2倍必要です。
PRでは細かいことは気にせずstd::wstringに変更しています。

CDocTypeManager::ConvertTypesExtToDlgExtでは書き込みバッファの最大値がわからないため、std::wstringで返すようにリファクタリングしました。

テスト内容

共通設定ー編集「Vistaスタイルのファイルダイアログ」をON/OFFすると表示に使う実装クラスが異なるため、ON/OFF2回ずつテストが必要です。
自動テストがなくて申し訳ないです。

テスト1

手順
下記マクロを実行して、異常終了しないことを確認します。

マクロでFileOpenDialog/FileSaveDialogのファイル名のデフォルト指定が長いと異常終了していたのが、マクロ中断のエラーダイアログが表示されるようになります。

var s = FileOpenDialog("file_name_long_long_0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456.js", "*.js");

テスト2

手順
同マクロで、拡張子フィルターが長いと異常終了していたのが、直ります。
特にいまのところ拡張子フィルターの文字列長に制限はありません、

var s = FileOpenDialog("file_name.js", "*.js;*.long_long_ext_filter_012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567");

テスト3

手順
フォルダを開くマクロのデフォルト指定がMAX_PATHを超えていたときに落ちていたのが、マクロ中断のエラーダイアログが表示されるようになります。

FolderDialog("バッファチェックのテスト", "C:\Program files\デフォルト表示フォルダ名123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678900123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890")

テスト4

タイプ別設定の拡張子に以下を設定:
a;b;c;d;e;f;g;h;i;j;k;l;m;n;o;p;q;r;s;t;u;v;w;x;y;z;0;1;2;3;4;5

拡張子が異常に長いファイル(存在していなくてもいい)を開きます。
ファイル名例:
D:\testfile.long_long_ext_filter_01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234

上記のタイプ別を一時適用します。

「名前を付けて保存」でダイアログを表示します。
落ちないか確認します。
また拡張子リストが正しく表示されるか確認します。

ダイアログを閉じた後に異常終了することがありました、のでそこまで確認します。

関連 issue, PR

参考資料

@usagisita
Copy link
Contributor Author

ちょっとPRが大きくなってしまったので、どうしようか悩みましたが、とりあえずPRを投げて、分割したらいいのか、所感など様子見をしたいです。

このままで問題ないのであれば、このPRの状態でレビューをしていただして、先に進めたいと思います。
よろしくお願いします。

@AppVeyorBot
Copy link

Build sakura 1.0.3585 completed (commit 717e9765b1 by @usagisita)

@beru beru added the 🐛bug🦋 ■バグ修正(Something isn't working) label Mar 21, 2021
Copy link
Contributor

@berryzplus berryzplus left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PRの趣旨には賛成で、見た限りではなんとなく良さそうに見えます。

とりあえず、👇についてコメントをお願いします。

Code Smell A 8 Code Smells

既存CodeSmellsが2万件もあるので誤差レベルといえばそうなんですけど、修正した範囲で新たに入れるものはなるたけ避けたいです(取り除くのが無理な場合もあるのでその時は「無理です」と言ってください。)

// sDefaultの先はSFilePath型
if (MAX_PATH <= sDefault.length()) {
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このファイルの変更は、他の変更と無関係なのでバラすことができます。(「バラせ」とは言いません。)
他の変更と異なり、かなり頑張らないとテストコードを書けない点も他と異なります。

@@ -51,7 +51,7 @@ class CDocTypeManager{

static bool IsFileNameMatch(const WCHAR* pszTypeExts, const WCHAR* pszFileName); // タイプ別拡張子にファイル名がマッチするか
static void GetFirstExt(const WCHAR* pszTypeExts, WCHAR szFirstExt[], int nBuffSize); // タイプ別拡張子の先頭拡張子を取得する
static bool ConvertTypesExtToDlgExt( const WCHAR *pszSrcExt, const WCHAR* szExt, WCHAR *pszDstExt ); // タイプ別設定の拡張子リストをダイアログ用リストに変換する
static bool ConvertTypesExtToDlgExt(const WCHAR *pszSrcExt, const WCHAR* szExt, std::wstring& destExt); // タイプ別設定の拡張子リストをダイアログ用リストに変換する
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

static関数で内部的なインスタンス依存もないので、挙動を確認するテストを書けそうです。
引数 std::wstring& destExt を戻り値 std::wstring に変えてしまったほうが、呼出元コードがすっきりすると思いました(変換失敗は戻り値.empty()で判定できますし。)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

戻り値にしてみました。テストコードも追加しました。

sakura_core/doc/CDocFileOperation.cpp Outdated Show resolved Hide resolved
sakura_core/doc/CDocFileOperation.cpp Show resolved Hide resolved
sakura_core/CFileExt.h Show resolved Hide resolved
@@ -41,7 +41,7 @@ class CFileExt
{
public:
CFileExt();
~CFileExt();
~CFileExt() = default;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

独自デストラクタを削除できるのであれば、独自コンストラクタも削除して Rule-of-Zero に従うのがベターだと思います。SonarCloudのBugsレベル警告のうちCriticalで指摘されるものがこの件で、5種類セットで書くか何も書かないかどちらかにしないとCritical警告が出るみたいです。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

デストラクタは消して、コンストラクタはdefaultにしました。
defaultを書いておかないと、コンパイラにエラーで怒られてしまいます。

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

自分が言ってたのは「何も書かないにできないか?」でした。
コンストラクタをdefaultにする必要があるなら、5種類セットで書く感じになります。

👇5種類

CFileExt() = default;
CFileExt(const CFileExt&) = delete;
CFileExt& operator = (const CFileExt&) = delete;
CFileExt(CFileExt&&) noexcept = delete;
CFileExt& operator = (CFileExt&&) noexcept = delete;
virtual ~CFileExt() noexcept = default;

あれ?6種類ある・・・。

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

細かい仕様ははっきり知らなかったので、ちょっと調べてみたのですが、
http://en.wikipedia.org/wiki/Rule_of_three_%28C%2B%2B_programming%29
https://en.cppreference.com/w/cpp/language/rule_of_three
を見たところ「普通のコンストラクタ」が5個には含まれない、つまりコンストラクタは存在していていい、ということみたいですね。
Rule of zeroのところにコンストラクタだけ宣言されています。

書くなら全部かけRule of fiveは、デストラクタを書くなら、コピーとムーブ演算子を定義するかdeleteする、という意味ですね。コピーやムーブ以外の、普通のコンストラクタは無関係かと。

@usagisita usagisita force-pushed the feature/fix_file_dialog_limit branch from dfa3f75 to 60008c8 Compare March 21, 2021 09:50
@usagisita
Copy link
Contributor Author

なんか単体テストがエラー吐いていて、ダメみたいですね。

@AppVeyorBot
Copy link

Build sakura 1.0.3588 failed (commit 71b3f7465e by @usagisita)

@berryzplus
Copy link
Contributor

なんか単体テストがエラー吐いていて、ダメみたいですね。

vs2019 を使って x64 - Debug ビルドしたモジュールで動かしたらテスト失敗が再現しました。

これ。
CDlgOpenFile::CommonItemDialogCreate
失敗原因はクラッシュみたいです。
なんとなく、テストが正しく実装されてないように見えます・・・。

@berryzplus
Copy link
Contributor

テストがクラッシュする原因は、CDlgOpenFile_CommonItemDialogのコンストラクタが GetDllShareData() を呼びだしていることのようです。現状では、グローバル変数が初期化されていない状態で共有メモリ取得のためのグローバル GetDllShareData() を呼びだした場合には正常動作として「クラッシュ」するようになっています。サクラエディタのダイアログは、基本的に共有メモリに依存する設計になっており単体テストを記述することは不可能です。

たぶんテストを書けるのは CFileExt とか CDocTypeManager::ConvertTypesExtToDlgExt とかのグローバル依存がないコードだけだと思います。

@berryzplus
Copy link
Contributor

テスト失敗の対策案を出しときます。

TEST(CDlgOpenFile, Construct)

 👇

TEST(CDlgOpenFile, DISABLED_Construct)

他の TEST(CDlgOpenFile,で始まる行にも同様の修正をすれば、テスト可能な部分だけが実行される状態になります。
せっかく書いたテストを消すのももったいないので、GetDllShareData() の問題を解決できるまで無効にしとけばよいと思います。

@usagisita
Copy link
Contributor Author

コメントありがとうございます。m_pShareDataの初期化を遅延させればと浅知恵しましたが、Createの後はいくつかの関数に分かれて呼び出されるため、バラバラに書くのも微妙だなという結論に達しました。
おとなしく教えてもらったように、DISABLEにしておきます。

@AppVeyorBot
Copy link

Build sakura 1.0.3595 completed (commit 330d80cf65 by @usagisita)

@AppVeyorBot
Copy link

Build sakura 1.0.3597 completed (commit 20e66183de by @usagisita)

@usagisita
Copy link
Contributor Author

Code Smellsのうち1件は直前の修正のミスだったので、対応しました。
残り1件のほうは、処理構造によるもの(for/if/whileの階層が深い)だったので、中身をリファクタリングしないと無理なので、今回は無理ということで、お願いします。

@param szExt [in] リストの先頭にする拡張子 例「.h
@param pszSrcExt [in] 拡張子リスト 例「.c cpp;.h」(ドットはオプション)
@param szExt [in] リストの先頭にする拡張子 例「.h」(ドット必須)
@param destExt [out] 拡張子リスト 例「*.h;*.c;*.cpp
Copy link
Member

@suconbu suconbu Mar 21, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

最後の @param destExt ... の所は @return になると思います。

Comment on lines +59 to +64
TEST(CDocTypeManager, ConvertTypesExtToDlgExtThree)
{
const std::wstring expected = { L"*.txt;*.cpp;*.h;*.hpp" };
std::wstring actual = CDocTypeManager::ConvertTypesExtToDlgExt(L"cpp;h;hpp", L".txt");
EXPECT_EQ(expected, actual);
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pszSrcExt の中に szExt の拡張子を含むパターン (例:ConvertTypesExtToDlgExt(L"txt;cpp;h;hpp", L".txt");) も用意されていた方が良いのではと思いました。

@AppVeyorBot
Copy link

Build sakura 1.0.3601 completed (commit 45efb69175 by @usagisita)

@sonarcloud
Copy link

sonarcloud bot commented Mar 22, 2021

@AppVeyorBot
Copy link

Build sakura 1.0.3602 completed (commit aa426b6117 by @usagisita)

@usagisita
Copy link
Contributor Author

長くお付き合いくださり、ありがとうございます。
コミットしようと思います。

@usagisita usagisita merged commit 4a64e53 into sakura-editor:master Mar 22, 2021
@usagisita usagisita deleted the feature/fix_file_dialog_limit branch March 22, 2021 13:45
@berryzplus
Copy link
Contributor

あえて揚げ足取っときますが マージ ですね。

git commit = 変更をローカルブランチに保存する。
git push = ローカルブランチをリモートに送信する。
マージ = ブランチの変更を master などに取り込む。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
🐛bug🦋 ■バグ修正(Something isn't working)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants