Skip to content

m6 command line interface_ja

Kazushi Kamegawa edited this page Aug 8, 2026 · 1 revision

M6: コマンドラインインターフェース

状態: 提供済み。issue #8 と sub-issue #53〜#57、加えてこのレビューが 追加したルール堅牢化のsub-issue #105 で追跡。6件すべてがスタックされた ブランチチェーン上のPR #107〜#112として提供され、マージ済み。 syncwingetlink.exe は#56の時点で初めてリンクに成功する。

このページに対する後の2回のレビューが、マージ済みコードに照らして修正を 加えた:

  • 提供順序の4番目の項目は、issue が存在する前に書かれたプレースホルダーでは なく、今は #105 とその実際のブランチを名指ししている; および
  • 「これらのPRが運ぶドキュメント」セクションのADR一覧が誤っていた - M6は(このページが当初予測した2件ではなく)6件のADR(ADR-0020〜ADR-0025) を生成し、ADR-0020とADR-0021に割り当てていた主題は docs/adr-phase-5.md が実際に記録している内容と一致していなかった。以下で 修正済み。

M6マージ後に見つかった1つの乖離は M6 が修正すべきものではなく、別途 追跡されている: --tui、--verbose、--quiet は AppOptions にパースされる ものの、どの消費者にも読まれないため、実行時に静かに無視される一方で --help と README.md はそれらが動作しているかのように提示している。 issue #113 とM8の計画ページを参照。

スコープ

M6は、プロセス外からの入力 - argv、ユーザー提供の rules.json、M2の パッケージソースがファイルシステムから収集したファイル名 - を受け付ける 最初のコードであり、端末に書き込む最初のコードでもある。また、main.cpp を所有する唯一のマイルストーンでもあるため、プロセス全体のCOM apartment のライフタイムと最終的な終了コードの両方がここに実装される。

M6は以下をカバーする:

  • scan/fix/test-rule と、文書化されたすべてのオプションを AppOptions にパースする;
  • コンソール出力(Unicode、色、確認プロンプト、--yes);
  • スクリプト用の --json 出力;
  • core/ へのディスパッチと、すべての失敗を文書化された終了コードに マッピングする; および
  • --help/--version。

M6は対話式のTUIチェックリストや進捗表示(M7)を実装しない。

このページは元々、test-rule のファイル名 → マッチしたルール → エイリアス という表示を M3 の #40 に予約しており、#53 が名前をパースし、#56 が ディスパッチし、#40 が出力を所有するという構成だった。この境界は提供時に 崩れた: #56 の runTestRule()(src/cli/Dispatch.cpp)自身がフル ファイル名 → マッチしたルール名 → エイリアスの行を出力しており、これは まさに #40 が求めていたものだった。#40 はマージ済みコードに照らして レビューされ、既に実装済みとしてクローズされた。docs/TODO.md M3の チェックボックスはM8の#64で修正される。

セキュリティ契約

M6は、信頼できないデータが人間またはスクリプトに初めて到達するマイルストーン である。以下のすべてのルールは、どのsub-issueが実装するかに関わらず 成立しなければならない。

ルール 要件
エスケープ/制御文字のサニタイズ パッケージID、実行ファイル名、エイリアス名は攻撃者が影響を与えられる - Packages 配下にファイルを置ける者は誰でもその名前を選べる。そのような文字列を出力する(コンソールまたはJSON)前に、C0制御文字、ESC/CSIシーケンス、bidi override文字(U+202A-U+202E、U+2066-U+2069)を取り除くかエスケープする。VT処理が有効かどうかに関わらず適用する。M7がグローバルに有効化するため。
--json ストリームの純粋性 --json が指定されている場合、stdoutはJSONドキュメントのみを保持し他は何も含まない - 診断、警告、プロンプトはない。それ以外はすべてstderrへ。エスケープは厳密なJSON文字列エスケープであり、サロゲートポリシーは明示的に文書化されている(下記参照)。WideCharToMultiByte が偶然行うことに任せない。
非対話的stdinは同意ではない --yes なしの fix は確認を求める。閉じた/リダイレクトされたstdinでのEOFや、bareのEnterは決して「はい」として扱ってはならない - どちらも拒否を生じさせ、暗黙の実行をしてはならない。#56/#57のレビューで修正: ここでの拒否はそれ自体エラーではなく終了コード3にマッピングされない - 拒否された/EOFされた候補は単にドライランモードで実行される(WouldCreate/WouldReplaceBrokenを再利用する)。他のoutcomeと同様に終了0/10に寄与する。同意に関わる終了3のケースは唯一パース時のもの: --json を --yes なしの fix と組み合わせる(ArgParseErrorKind::ConflictingOptions)。これはプロンプトが起こりうる前に拒否される。
信頼できないrulesファイル入力は境界を持つ --rules またはユーザールールパス経由で読み込まれるrulesファイルは信頼できない。loadRuleSetFromFile() は、ファイル全体を読む前にファイルのバイトサイズを上限で切る必要がある; RuleSet(std::vector<AliasRule>) はルール数とフィールド(name/pattern/replacement)ごとの長さを上限で切る必要がある。RuleSet::resolve() が呼ぶ std::regex_match は std::regex_error に対して try/catch で包む必要がある - MSVCの std::regex はコンパイル時だけでなくマッチ時にも error_complexity/error_stack を送出しうるため、今日はコンパイルパス(RuleSet.cpp:176)のみがガードされておりマッチパス(RuleSet.cpp:290/297)はガードされていない。これらはすべて、パースエラーと同様に終了コード3に集約される。これは rules/ に対する自己完結的な変更であり、#53に折り込まず独自のsub-issueとして追跡する。
パスの上書きは検証されるが、信頼はされない --links-dir、--packages-dir、--rules は、空であるか \\.\ デバイスパスに解決される場合は拒否され、それ以外は保存前に絶対パス化(std::filesystem::absolute + lexically_normal)される。このレビューの最初のパスからの修正: それらは既に存在することを要求されない。存在しないPackagesディレクトリは通常の許容される状態である - FsScanSource はそれを失敗ではなくパッケージ0件として報告する(ADR-0010) - また存在しないLinksディレクトリは、まさに fix が存在する理由となる状態である。どちらかをパース時に拒否することは、コードベースの他の場所で確立された挙動と矛盾する。--rules の存在/可読性チェックは引き続き RuleSetSelector の仕事である(ADR-0013はすでに不在-フォールスルー vs 不正-致命的を区別している)。ArgParserはそのチェックを重複させない。解決済みの実効パスは、変更を伴う操作が実行される前にエコーされる。Paths::getLinksDirectory/getPackagesDirectory は引き続きデフォルトパスの唯一の情報源であり、SHGetKnownFolderPath(FOLDERID_LocalAppData) を使う - 呼び出し環境によって偽装されうる %LOCALAPPDATA% の環境変数読み取りは決して使わない。ADR-0020を参照。
自己昇格なし マニフェストは asInvoker のままである(src/app.manifest)。InsufficientPermission の終了パスで、M6はガイダンス - Developer Modeを最初に、昇格を次に - を表示し、ShellExecuteW/runas を自ら呼び出すことはない。昇格した再起動は「別のユーザーとして実行」の下で別のユーザーの %LOCALAPPDATA% を解決してしまい、どの Links/Packages ディレクトリが使われているかを静かに変えてしまう。
衝突は決して fix に到達しない M5のWikiページは、M5の SymlinkService が衝突しているエイリアスの間で勝者を選ばないこと、M6/M7が repairLink() を呼ぶ前に衝突している候補を修復セットから除去しなければならないことを明確に述べている。--yes はこれを迂回してはならない - 衝突は報告されるのみで、自動解決されない。
test-rule NAME はベアなファイル名を取る #53は既存の isValidAliasFileName()(src/rules/RuleSet.h)で NAME を検証してからそれとして扱う - パス区切り文字もドライブ文字も不可。そのエコーは他のファイル名と同じサニタイザを通す。
トップレベルの例外は捕捉されマッピングされる wmain から漏れる例外は未文書化のクラッシュ終了コード(/GS 下での 0xC0000409)を生む。wmain はその本体を try/catch で包み、既知のすべての例外型を下記の終了コードテーブルへマッピングし、それ以外は汎用の非ゼロ失敗にマッピングする - 未処理の例外でプロセスが終了することを決して許さない。
エントリポイントのDLL検索堅牢化 wmain の最初の文は SetDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32) であり、実行ファイルプロジェクトは /DEPENDENTLOADFLAG:0x800 を設定する。現在のインポートのほとんど(kernel32、combase、shell32)はすでにKnownDLLであるため、これが閉じる実際の露出は小さい - これは実証された脆弱性への対処ではなく、M8のloose-exeリリースに先立つ安価な多層防御であり、受け入れ基準はこれを過大に主張すべきではない。

Win32契約

ルール 要件
エントリポイント wmain(int argc, wchar_t* argv[])。ワイド引数を取るコアの run() の薄いシムとして保つ - main.cpp は実質的なロジックを持てない。MSTest DLLは実行ファイルのオブジェクトファイルをリンクできないため(ADR-0002)、また狭い main の内部で GetCommandLineW()/CommandLineToArgvW からワイドargvを再導出することは、wmain が無償で回避しているクォーティングのバグを再導入するだけになる。
コンソール出力の選択 出力ハンドルで GetConsoleMode を使って探査する。実際のコンソールは WriteConsoleW を得る; リダイレクトされたハンドルは WriteFile 経由のUTF-8バイトを得る。以前のコードページのRAII復元なしに SetConsoleOutputCP(CP_UTF8) をプロセス全体に設定してはならない - さもないと対話的な親コンソールでプロセスより長く生存してしまう。_setmode(_O_U16TEXT) を同じストリームでの狭バイト書き込みと混在させてはならない。非常に大きな書き込みはチャンク化する。WriteConsoleW は過大なバッファで ERROR_NOT_ENOUGH_MEMORY を伴って失敗しうるため。
ハンドルの検証 GetStdHandle は NULL(ハンドルなし)または INVALID_HANDLE_VALUE(閉じた/切り離されたディスクリプタ)を返しうる。どちらも、そのままWin32 I/O呼び出しに渡すのではなく「使用可能なストリームではない」として扱わなければならない。
プローブされた機能としての色 色/VTサポートは SetConsoleMode(handle, mode | ENABLE_VIRTUAL_TERMINAL_PROCESSING) を試みて結果を確認することで確立される - 決して仮定しない。元のモードはすべての終了パスでRAIIにより復元される。リダイレクトされた出力は色を完全に無効化する; NO_COLOR(任意の値)と新しい --no-color フラグは、どちらもTTY状態に関わらずそれを強制的にオフにする。M6は検出/復元を所有し、フルVT駆動のUIはM7が所有する。
プロンプト入力 確認プロンプトは、ENABLE_LINE_INPUT | ENABLE_ECHO_INPUT を設定した実際のコンソールハンドル上で ReadConsoleW を通して読み取り、元のモードは後で復元される; リダイレクトされたstdinはバイトとして読まれUTF-8デコードされる。ここが、セキュリティ契約の「EOF/bareのEnterは同意ではない」ルールが強制される場所でもある。
Ctrl+Cの処理 SetConsoleCtrlHandler はコンソールモードを復元し、バッチを(項目の途中ではなく)項目間で停止させ、定義された終了コードを生む。すでに実行中の CreateSymbolicLinkW 呼び出しはキャンセル不能である - ガイダンステキストはそれを暗示してはならない。
既存のエラーメッセージをUTF-8としてデコード PackageSourceError、RuleSetError、SymlinkServiceError のメッセージは、(コア内の WideCharToMultiByte(CP_UTF8, …) 経由で)UTF-8の std::string として構築される。Console はプロセスのACPではなく CP_UTF8 でデコードしなければならない。さもないとエラーテキスト中の非ASCIIパスが壊れる。
相対パスの上書きは絶対で保存される Paths::toExtendedLengthPath() は、プレフィックスを付ける前に相対パスをすでに絶対化している(相対パスに盲目的にプレフィックスを付けるわけではない - このレビューの最初のパスからの修正、既存実装を誤って特徴づけていた)。ArgParserは引き続き --links-dir/--packages-dir/--rules をパース時に絶対パスへ正規化するが、それは独自の理由による: AppOptions 内の安定した表示可能な値であり、それがエコーまたは使用される時点でのプロセスのカレントディレクトリが何であっても独立している。すでに toExtendedLengthPath() を通過したパスをユーザーに表示する際は fromExtendedLengthPath()(src/core/Paths.h)を使う。
プロセス全体で単一のCOM apartment main.cpp は他のどのコア呼び出しより先に、プロセスの生存期間を通じて厳密に1つの core::ComApartment を構築する - RuleSet::parse() は --source fs や test-rule に対しても初期化済みのapartmentを必要とする(ADR-0011: winrt::Windows::Data::Json を使うため)。WingetComSource は現在自身のapartmentを所有しているが、それは main.cpp がまだ存在しないためにすぎない。ADR-0009はM6にその所有権をここへ移すことを明示的に求めている。どのsub-issueにも現時点で割り当てられていない - このレビューは#56に割り当てる。
終了コードマッピングの網羅性 ディスパッチ層の終了コード関数は PackageSourceErrorKind、RuleSetErrorKind、SymlinkServiceErrorKind に対して網羅的でなければならない - 今日は PackageSourceErrorKind にマッピング先が一切ない。下記の表を参照。

終了コードマップ

0/1/2/3/10 は docs/PLAN.md §8 と AGENTS.md §6 ですでに文書化 されているコードである。このレビューは**4**を追加する。明示的な --source com の失敗(サーバー不在、ポリシーブロック)がこれまでどこにも マッピングされていなかったために必要である - 3 は「引数/設定が間違っている」 を意味するが、データソースの障害はそれではない。また 10 は「一部の修復が 失敗した」を意味するが、実行に至らなかった scan には適用されない。したがって scan は 0、3、4 のいずれかを返しうるが、決して 10 は返さない。

コード 意味 発生条件
0 成功 修復すべきものがない、または fix が完全に成功した
1 修復が必要だが実行されなかった scan --fail-on-missing がMissing/Broken/Mismatchの候補を見つけた
2 権限不足 SymlinkServiceErrorKind::InsufficientPermission
3 引数/設定エラー 未知/不正なCLIオプション; いずれかの RuleSetErrorKind(新しいマッチ時 regex_error キャプチャを含む); 不正な test-rule 名; 検証に失敗したパスの上書き; --json を --yes なしの fix と組み合わせる(パース時の衝突 - 実際の fix 実行中に拒否/EOFされた確認は通常の拒否であり、このコードではない。上記のセキュリティ契約の修正を参照); 予期しない LinkInspectionError(リンク検査の失敗には固有の PackageSourceErrorKind がないため、4 ではなくこの汎用バケットに入る)
4 パッケージ列挙失敗 明示的な --source com または --source fs により表面化した任意の PackageSourceErrorKind(④auto がFSへdegradeするのは PackageSourceError によるものであり、それ自体は失敗ではない - ADR-0010参照 - したがって auto はFSも失敗した場合のみ 4 に到達する)
10 一部の修復が失敗 他は進捗していたバッチ中の、InsufficientPermission 以外の1つ以上の SymlinkServiceErrorKind 値(DeleteFailed/CreateFailed/VerificationFailed)

fix 中のCtrl+Cは(コンソールモードを復元し進行中の項目を完了した後) 非ゼロで終了する(10 を再利用する。部分的なバッチはまさにそのコードが 既に意味するものであるため)。

終了コード2を生む4つの (elevation, developerMode) の組み合わせに対する ガイダンステキスト(ADR-0019がM6の表示のためにこれをここに配置する):

  • 非昇格 + Developer Mode 無効 → Developer Modeを有効にするか、昇格して 実行するよう提案;
  • 非昇格 + Developer Mode 不明 → Developer Modeを確認するか、昇格して 実行するよう提案;
  • 昇格 + 権限失敗 → 昇格済みトークンまたはローカルポリシーがまだ シンボリックリンク権限を欠いていることを述べる;
  • アクセス拒否 → Links ディレクトリへの書き込み/削除アクセスを確認する よう提案。

これらのガイダンスパスのどれも自己昇格しない(上記のセキュリティ契約)。

提供順序

  1. #53 — コマンドとオプションをパースする。 cli/ArgParser: scan/fix/test-rule と、docs/PLAN.md §8のすべてのオプションに 加え、--fail-on-missing と新しい --no-color(どちらも今日の§8には 欠けている - この issue の一部として修正済み)。パスの上書き (セキュリティ契約)と test-rule 名を検証する。未知のオプション、 必須値の欠如、--yes なしの --json + fix はすべて終了コード3で パースに失敗する。-- ターミネータをサポートする。%LOCALAPPDATA% を直接読むことは決してない。 ブランチ: feature/53-parse-commands-and-options。
  2. #54 — コンソールインタラクションを実装する。 cli/Console: Win32契約の出力選択、ハンドル検証、色のプローブ/復元、プロンプト入力、 セキュリティ契約が求めるサニタイザ。--yes はプロンプトを迂回するが 衝突チェックは迂回しない。 ブランチ: feature/54-console-interaction。
  3. #55 — マシン可読なJSONを出力する。 stdout専用のJSON、厳格な エスケープ、文書化されたサロゲートポリシー、BOMなし、docs/PLAN.mdに 記録された安定したスキーマ。--json が設定されているとき診断はstderr へ移る。 ブランチ: feature/55-json-output。
  4. #105 — 信頼できないrulesファイル入力を堅牢化する。 セキュリティ 契約からのrulesファイルの上限と RuleSet::resolve() の try/catch。cli/ ではなく rules/ に置く。単独でレビュー可能な 変更にするため; #56はその終了コードの挙動に依存する。 ブランチ: feature/105-harden-rules-input。
  5. #56 — コマンドと終了コードをディスパッチする。 main.cpp: 単一の ComApartment の構築、トップレベル例外ガードを伴う wmain、 SetDefaultDllDirectories、終了コード 4 を含む総合終了コードマップ、 自己昇格なしのガイダンステキスト、SymlinkService への呼び出しの前の 衝突除外ゲート。 ブランチ: feature/56-dispatch-and-exit-codes。
  6. #57 — ヘルプとバージョン出力を提供する。 --help を終了0で stdoutへ、終了コードテーブルをヘルプテキストに再現する; --version を 終了0で単一の信頼できる情報源からstdoutへ; その他のパース失敗は 終了3でusageをstderrへ出力する。ネットワークアクセスも ShellExecute もない。 ブランチ: feature/57-help-and-version。

#53は他のすべてのsub-issueの AppOptions が依存する基盤である。#54、#55、 #105は#53が着地した後は独立して進行できる。#56はこの3つすべてに依存する。 #57は#53が着地すれば#54/#55と並行して進行できる。ヘルプ/バージョンは AppOptions パースを、フラグの認識以上には触らないため。

テスト計画

  • サニタイザ — パッケージID/ファイル名/エイリアスに埋め込まれた エスケープシーケンス、C0制御文字、bidi override文字が、コンソールと JSON出力の両方で無害化されること。
  • JSONエスケープ — 引用符、バックスラッシュ、制御文字、非BMP文字、 対になっていないサロゲートがすべて、静かな置換ではなく文書化された 安定した出力を生むこと。
  • 終了コードの網羅性 — テーブル駆動のテストが、すべての PackageSourceErrorKind、RuleSetErrorKind、SymlinkServiceErrorKind の列挙子が正確に1つの文書化されたコードにマッピングされることをアサート すること。
  • 非対話的な同意 — 閉じた/リダイレクトされたstdinとbareのEnterが どちらも --yes なしの fix を拒否すること(進行するのではなく終了 コード3)。
  • パス上書きの検証 — 空の値と \\.\ デバイスパスが拒否されること; 相対パスが受理され絶対に正規化されること; まだ存在しないパスが拒否 ではなく受理されること。存在しないPackages/Linksディレクトリに対する ADR-0010の許容と一致する。
  • rules入力の境界 — 過大なrulesファイル、過大なルール数、マッチ時に 送出する病的なパターンが、すべて未捕捉の例外ではなく終了コード3に 解決されること。
  • 衝突の除外 — 衝突を含む修復セットが、--yes の有無に関わらず SymlinkService::repairLink() に決して到達しないこと。
  • test-rule 名の検証 — パス区切り文字やドライブ文字を含む引数が、 runTestRule() のプレゼンテーションロジックが実行される前に拒否される こと。

各プルリクエストは、警告をエラー扱いにする設定で Debug|Release × x64|ARM64 でコア、cli、テストをビルドする。DebugとReleaseのx64テストは vstest.console.exe を通して実行し、実際の結果を報告する。ARM64は ARM64ホストで実行しない限りクロスビルドのみとして報告する。サードパーティ 依存は導入しない。

これらのPRが運ぶドキュメント

  • docs/PLAN.md §8: --fail-on-missing と --no-color をオプション表に 追加、終了コード表に終了コード4を追加。
  • docs/TODO.md M6: 各sub-issueが着地するたびに項目をチェックし、それぞれ 完了させたPRを指す(M4/M5のクローズスタイルに合わせる)。
  • AGENTS.md §6: そこにも終了コード4を表に追加。
  • README.md: CLI使用法/終了コードセクションを一致させて更新。
  • docs/adr-phase-5.md(新規)。このページは元々2件のADRを予測しどちらも 誤って記述していた; M6は実際には6件を生成し、それらが記録するものは 以下のとおり:
    • ADR-0020 — CLI引数モデル、そしてこのレビューの最初の草稿から 絞り込まれたパス上書き検証範囲(上記セキュリティ契約の 「検証されるが存在は要求されない」修正)。
    • ADR-0021 — Console: 操作の縫い目、サニタイズ範囲、そしてなぜ 入力モードはプロセス全体ではなく呼び出しごとに復元されるのか。 加えて、コアが生成する診断テキストのUTF-8規約。
    • ADR-0022 — JSONスキーマ、文字列エスケープルール、明示的な サロゲートポリシー。--json の下でのstdout純粋性を含む。
    • ADR-0023 — rulesファイル入力の境界: サイズ、ルール数、フィールド長 の上限、加えてマッチ時の regex_error ガード(#105)。
    • ADR-0024 — main.cpp: (新しいコード4を含む)総合終了コード マップ、scan/fixパイプライン、WingetComSource からの ComApartment 所有権の移動、トップレベル例外ガード、SetDefaultDllDirectories / /DEPENDENTLOADFLAG:0x800 エントリポイント堅牢化。
    • ADR-0025 — 単一のバージョン定数(cli::kVersion)とヘルプテキストの 磨き上げ。src/app.manifest のバージョンがビルド時にそれとリンクされ なかった理由を含む。

完了基準

  • #53〜#57、および#105がマージ・クローズされ、issue #8がその完了を 反映している。
  • 上記のセキュリティ契約とWin32契約のすべてのルールが満たされ、上記の 計画のテストでカバーされている。
  • 終了コードマップが網羅的であり、PackageSourceError.h、RuleSet.h、 SymlinkService.h と正確に一致する。
  • docs/PLAN.md、docs/TODO.md、AGENTS.md、README.md、 docs/adr-phase-5.md、このWikiページが一致している。
  • 正典ドキュメントとコメントは英語である。ローカライズされた *_ja.md ファイルは読み取り・変更しない。

Clone this wiki locally