/**
* CSV の共通部品(セルの整形・文書の組み立て)。
*
* ★清掃詳細ログ(`lib/clean-logs/csv.ts`)とレポートの施設別パフォーマンス
* (`lib/reports/facility-performance-csv.ts`)が同じ関門を通るように、ここへ集めてある★(D-163)。
* 数式インジェクション対策(D-125)を CSV ごとに書き直すと、片方だけ直して
* もう片方が抜ける。`csvCell()` は `lib/clean-logs/csv.ts` から★コメントごと★移した。
*/
import { displayRate } from "@/lib/judge";
/**
* 数式として解釈されうる先頭文字。
*
* ★表計算ソフト(Excel / Google スプレッドシート)は、セルの先頭が
* `=` `+` `-` `@` だとその中身を★数式★として解釈する★。
* これがいわゆるCSVインジェクションで、CSVを開いた人の手元で意図しない処理が動く。
*
* ★TAB(0x09) と CR(0x0D) も入れてある★ — OWASP の CSV Injection の対策は
* この2つも先頭文字として挙げている。取り込み時に前後の空白が落とされた結果、
* ★次の文字(`=` 等)が先頭に来る★ため、上の4文字と同じ危険がある。
* - TAB始まりは★下の引用判定(`/[",\n\r]/`)に引っかからない★ので、ここで
* 前置しないと★何の加工も受けずに素通りする★
* - CR始まりは引用はされるが、★表計算ソフトは引用符を外してから中身を評価する★ので
* 引用だけでは止まらない。`'` の前置が要る
*
* ★値の「途中」にある TAB / CR は対象外★ — 数式として解釈されるのは先頭だけで、
* 途中まで加工すると正常な値まで書き換えてしまう(過剰防御)。
*/
const FORMULA_PREFIXES = ["=", "+", "-", "@", "\t", "\r"];
/**
* CSVの1セルを整形する。
*
* 処理は2段階で、★この順序に意味がある★。
*
* 1. ★数式インジェクション対策★ — 先頭が `FORMULA_PREFIXES` のいずれか
* (`=` `+` `-` `@` `TAB` `CR`)なら `'` を前置して
* 「文字列である」と表計算ソフトに伝える。
* ★対策を入れた理由★(指示書AE 段階2-4 / D-125):
* CSVに出る値のうち施設名・機種名・シリアルは★メインDB由来★で、
* そこへ書いているのは★TeamWorks のデータ収集処理★(`CLAUDE.md` §2)。
* つまり★DFA側では中身を保証できない★。P1の利用者はDFA社内のみだが、
* 保証できない値をそのまま数式として実行させる理由もないため、
* `csvCell()` の内側だけで完結する対策を入れてある。
*
* 2. `"` / `,` / 改行を含むなら全体を `"` で囲み、`"` を `""` に置換する(RFC 4180)。
*
* ★1→2 の順でないと、付けた `'` が引用符の外に出て効かなくなる★。
*/
export function csvCell(value: string): string {
const guarded = FORMULA_PREFIXES.some((p) => value.startsWith(p)) ? `'${value}` : value;
if (/[",\n\r]/.test(guarded)) {
return `"${guarded.replaceAll('"', '""')}"`;
}
return guarded;
}
/**
* CSV の率のセル(四捨五入の整数+`%`)。★値が無い(null)ときは空欄★。
*
* ★空欄にする理由★(ユーザー指示 2026-10-06「値がないとこは空欄で」。D-164):
* 画面用の `displayRate()` は null で `-` を返す。それをそのまま CSV に出すと、
* 数式インジェクション対策(`csvCell()`。D-125)が先頭の `-` に `'` を前置して `'-` になり、
* ★Excel で開くと `'-` がそのまま見えていた★。CSV では「値が無い」を空のセルで表す。
*
* ★丸めは表示のみ★(CLAUDE.md §4.3)— 判定には使わない。値があるときは `displayRate()` と同じ文字列。
* ★清掃詳細ログとレポートの施設別パフォーマンスの両方の CSV がこれを使う★(片方だけ直して抜けないように)。
*/
export function csvRate(rate: number | null): string {
return rate === null ? "" : displayRate(rate);
}
/**
* CSV 文書全体を組み立てる(UTF-8 BOM付き・CRLF区切り・最後の行も CRLF で終える)。
*
* ★データ行は全セルを例外なく `csvCell()` に通す★(D-125)。
* 呼び出し側は「生の文字列を並べるだけ」にして、列ごとに安全かどうかを判断させない。
* 「この列は固定文言だから安全」という判断を列ごとにすると、後から列を足した人が
* 同じ判断を迫られて漏れる(`displayRate()` の `-` が実例。`lib/clean-logs/csv.ts`。
* ★いまは率の列は `csvRate()` で null を空欄にしている★(D-164)が、関門は引き続き全セルに通す)。
*
* ★ヘッダ行はエスケープしない★ — 見出しはコードに書いた固定の日本語で、
* 清掃詳細ログの元の実装(`CLEAN_LOG_CSV_HEADER.join(",")`)と出力を1バイトも変えないため(D-163)。
*
* 先頭のBOMは Excel が UTF-8 と判別するために必要(清掃詳細ログの route.ts から移設したもの)。
* 0件のときは `BOM + ヘッダ + CRLF` だけになる。
*/
export function csvDocument(
header: readonly string[],
rows: readonly (readonly string[])[]
): string {
const lines = [header.join(","), ...rows.map((r) => r.map(csvCell).join(","))];
return `${lines.join("\r\n")}\r\n`;
}