diff --git a/AGENTS.md b/AGENTS.md index be5eead..6b9d8c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -343,6 +343,7 @@ make ci # CI と同じ検査 (fmt-check + lint + test、書き換 - mise のグローバル・ローカル・ツール単位の明示 age と uv の `exclude-newer` は native age として読み、depup の制約より厳しい値を維持する。ツール単位の mise age はスコープ内の最厳値を全ツールに適用する。解釈できない native 設定があれば、そのマニフェストを更新しない。 - `go.work` の `use` と `settings.gradle` / `settings.gradle.kts` の `include` を展開してサブプロジェクトのマニフェストも検出する。ルートに `go.mod` / 依存宣言が無い構成では展開しないと「更新なし」と報告して**無言の no-op** になる (Cargo workspace members と pnpm workspace packages には自動展開があるので Go と Java だけ非対称だった)。展開先のパスは `.depup` と同じ安全性検査 (絶対パス・`..`・外部へ解決される symlink の拒否) を通す。検出ソースが増えたため、重複排除は `push_unique_manifest` に集約している。`include` のプロジェクトパスの先頭の `:` は省略できる (`include('app')` は `include(':app')` と同じで、`gradle init` も省略形で書く。以前は `:` を必須にして検出しなかった: #96)。Gradle のプロジェクト名に使えない `/` `\` と `.` / `..` の要素は展開しない - GitHub Tags の semver 受理判定は `SwiftVersionParser` へ委譲して `Package.swift` 側と一元化する。取得側だけが数値識別子の先頭ゼロを許していたため、`2024.01.15` のような CalVer タグが (semver タグと混在すると `2024 > 1` で必ず) 最新候補に選ばれ、SPM の `Version` が読めない `Package.swift` を書き込んでいた。書き込み側でも `new_version` の妥当性を検証する。タグが 1000 件 (10 ページ) を超えて打ち切られた場合は警告を出す (無言で切ると古い系列が候補から静かに落ちる) +- グローバル設定は `GlobalConfig::read_from` で読み、読み込めない・UTF-8 でない・TOML として解釈できないときは、どれも警告して組み込みの既定値を使う (#123)。以前は `read_to_string(...).ok()?` で読み、解釈できないときだけ警告していたので、読めないファイルや UTF-16 の設定は警告なしに無視され、`age` を既定より長くしていても黙って既定の age で更新先を選んでいた - `GITHUB_TOKEN` / `GH_TOKEN` は空文字を「未設定」として読み飛ばす。`std::env::var` は「セット済みだが空文字」に対して `Ok("")` を返すため、空値をそのまま採用すると `Authorization: Bearer ` (トークン部が空) を送って GitHub に 401 で拒否され、ヘッダを付けなければ未認証で成功していた取得が全滅する。空の `GITHUB_TOKEN` が置かれた環境で `GH_TOKEN` へフォールバックできない問題も同時に防ぐ - レジストリのパッケージ名は URL へ埋め込む前に文字種を検証する (npm / PyPI / crates.io / RubyGems / Packagist / Maven Central / GitHub Tags)。`reqwest` が使う `url` crate は WHATWG URL 仕様どおりドットセグメントを正規化するため、`"a/../lodash": "^1.0.0"` のようなキーがあると lodash の版を取得して元のキーへ書き戻す。`?` / `#` はクエリ・フラグメントとして解釈される。npm alias の実名はマニフェストの**値**から切り出されるため、名前の出所はキーだけではない - RubyGems の `created_at`、PyPI の `upload_time_iso_8601`、Packagist の `time`、Go proxy の `.info` の `Time` が欠落・不正な場合は公開日不明として保持する。age が有効なら日時不明の版を候補から外し、実在しない 1970 年を公開日として表示しない。mise は同じツール・ベンダーの他の版に日時があれば欠落版を除外し、全版に日時がなければ選択を許すが公開日不明と表示する。GitHub Tags は一覧に日時が無いので、Swift は age の判定に要る候補だけ公開日を後から取得する (Swift の項を参照) diff --git a/docs/configuration.ja.md b/docs/configuration.ja.md index def9809..0910f5c 100644 --- a/docs/configuration.ja.md +++ b/docs/configuration.ja.md @@ -47,7 +47,7 @@ osv_risk_tradeoff = true # github = ["example-dev"] ``` -ファイルにないキーには、組み込みデフォルト(`age = "1w"`、`osv = true`、`osv_risk_tradeoff = true`、`max_change` の制限なし)が使われます。ファイルを作成・解析できない場合は、警告を表示して組み込みデフォルトを使います。値が不正なキー(`age = "abc"` など)は、警告を表示したうえで、そのキーだけを組み込みデフォルトに戻します。 +ファイルにないキーには、組み込みデフォルト(`age = "1w"`、`osv = true`、`osv_risk_tradeoff = true`、`max_change` の制限なし)が使われます。ファイルを作成・読み込み・解析できない場合は、警告を表示して組み込みデフォルトを使います。ファイルは UTF-8 のテキストで書いてください。UTF-8 でないファイル(Windows PowerShell 5.1 の `>` が書く UTF-16 など)も同じく警告します。値が不正なキー(`age = "abc"` など)は、警告を表示したうえで、そのキーだけを組み込みデフォルトに戻します。 既存の設定ファイルに `osv_risk_tradeoff` がなくても、脆弱性を修正する更新は、脆弱性が残る場合も条件付きで許可します。件数の数え方と比較基準は「[脆弱性チェック](usage.ja.md#脆弱性チェックosvdev)」を参照してください。 diff --git a/docs/configuration.md b/docs/configuration.md index 2666c91..9304393 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -48,7 +48,7 @@ osv_risk_tradeoff = true # github = ["example-dev"] ``` -A key that is missing from the file falls back to the built-in default (`age = "1w"`, `osv = true`, `osv_risk_tradeoff = true`, no `max_change` limit). If the file cannot be created or parsed, depup prints a warning and uses the built-in defaults. If a single value is invalid (such as `age = "abc"`), depup prints a warning and uses the built-in default for that setting only. +A key that is missing from the file falls back to the built-in default (`age = "1w"`, `osv = true`, `osv_risk_tradeoff = true`, no `max_change` limit). If the file cannot be created, read, or parsed, depup prints a warning and uses the built-in defaults. Write the file as UTF-8 text; a file that is not UTF-8 (such as the UTF-16 that `>` writes in Windows PowerShell 5.1) is reported the same way. If a single value is invalid (such as `age = "abc"`), depup prints a warning and uses the built-in default for that setting only. Updates that fix existing vulnerabilities may still be allowed under severity and count limits, even when an existing configuration file has no `osv_risk_tradeoff` key. See [Vulnerability Check](usage.md#vulnerability-check-osvdev) for the counting rules and baseline requirements. diff --git a/src/global_config.rs b/src/global_config.rs index eebfedb..ca44fad 100644 --- a/src/global_config.rs +++ b/src/global_config.rs @@ -149,20 +149,38 @@ impl GlobalConfig { return None; } - let content = std::fs::read_to_string(path).ok()?; - match toml::from_str::(&content) { + match Self::read_from(path) { Ok(config) => Some(config), - Err(e) => { - eprintln!( - "Warning: failed to parse global config '{}': {}", - path.display(), - e - ); + Err(message) => { + eprintln!("Warning: {message}"); None } } } + /// 設定ファイルを読んで解釈する。読めない・UTF-8 でない・TOML として解釈できないときは、 + /// 警告の文言を返す (呼び出し側が警告して組み込みの既定値を使う)。以前は + /// `read_to_string(...).ok()?` で読み、読み込みに失敗したファイルや、Windows PowerShell 5.1 の + /// `>` が書く UTF-16 を、警告なしに「設定がない」と同じに扱っていた。`age` を既定より長く + /// していても、黙って既定の age で更新先を選んでいた (#123)。 + fn read_from(path: &Path) -> Result { + let bytes = std::fs::read(path).map_err(|error| { + format!("failed to read global config '{}': {error}", path.display()) + })?; + let content = String::from_utf8(bytes).map_err(|_| { + format!( + "failed to read global config '{}': the file is not UTF-8 text; save it as UTF-8", + path.display() + ) + })?; + toml::from_str::(&content).map_err(|error| { + format!( + "failed to parse global config '{}': {error}", + path.display() + ) + }) + } + /// 設定の `age` を [`Duration`] に変換する。 /// /// 未指定 or パース失敗の場合は `None`。パース失敗時は警告を出す。 @@ -249,6 +267,29 @@ pub fn resolve_osv(cli_osv: bool, no_osv: bool, config: Option<&GlobalConfig>) - #[cfg(test)] mod tests { + /// Issue #123: 読めない・UTF-8 でないグローバル設定も、解釈できない設定と同じく警告の + /// 文言を返す (読めた内容は、BOM 付きの UTF-8 でも読める)。 + #[test] + fn unreadable_or_non_utf8_global_config_is_reported() { + let dir = tempfile::tempdir().unwrap(); + let path = dir.path().join("config.toml"); + let utf16: Vec = [0xFEFF_u16] + .into_iter() + .chain("age = \"2w\"\r\n".encode_utf16()) + .flat_map(u16::to_le_bytes) + .collect(); + std::fs::write(&path, utf16).unwrap(); + let error = super::GlobalConfig::read_from(&path).err().unwrap(); + assert!(error.contains("not UTF-8"), "{error}"); + // 読み込みに失敗したとき (ディレクトリを指すなど) + let error = super::GlobalConfig::read_from(dir.path()).err().unwrap(); + assert!(error.starts_with("failed to read global config"), "{error}"); + // 同じ内容の UTF-8 (BOM 付き) は読める + std::fs::write(&path, "\u{feff}age = \"2w\"\r\n").unwrap(); + let config = super::GlobalConfig::read_from(&path).unwrap(); + assert_eq!(config.age.as_deref(), Some("2w")); + } + #[test] fn age_exempt_normalizes_logins_and_preserves_other_settings_on_errors() { let config: super::GlobalConfig = toml::from_str("age = '2w'\nosv = true\n[age_exempt]\ngithub = ['EXAMPLE-dev', 'example-dev', 'bad/name']").unwrap();