---
phase: 02-correctness
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - src/parser.rs
  - src/sqllog.rs
  - tests/sqllog_additional.rs
autonomous: true
requirements:
  - CORR-01
  - CORR-03

must_haves:
  truths:
    - "大文件（>64 KB）中出现的 GB18030 字节能被正确识别，不再误判为 UTF-8"
    - "SQL body 内含 EXECTIME:/ROWCOUNT:/EXEC_ID: 关键字时，body() 返回完整 SQL，不被截断"
    - "SQL body 内含伪指标关键字且无真实指标时，parse_indicators() 返回 None"
    - "存在真实指标时，已有解析行为不变"
  artifacts:
    - path: "src/parser.rs"
      provides: "编码检测扫描整个文件"
      contains: "let sample = &mmap[..];"
    - path: "src/sqllog.rs"
      provides: "find_indicators_split 验证候选切分点"
      contains: "parse_indicators_from_bytes(&data[split..]).is_none()"
    - path: "tests/sqllog_additional.rs"
      provides: "CORR-01 大文件越界回归测试 + CORR-03 五条假阳性测试"
      exports: []
  key_links:
    - from: "src/parser.rs::from_path"
      to: "simd_from_utf8"
      via: "let sample = &mmap[..]"
      pattern: "let sample = &mmap\\[\\.\\.\\]"
    - from: "src/sqllog.rs::find_indicators_split"
      to: "parse_indicators_from_bytes"
      via: "validation guard after rfind"
      pattern: "parse_indicators_from_bytes.*is_none"
---

<objective>
修复两个已确认的正确性 Bug，为后续热路径改动建立安全地基。

Purpose: CORR-01 消除 64 KB 截断导致大文件 GB18030 误判为 UTF-8 后的 UB；CORR-03 消除 find_indicators_split 在 SQL body 含指标关键字时的错误切分。
Output: 修复后的 src/parser.rs + src/sqllog.rs；tests/sqllog_additional.rs 新增 6 条测试（1 条 CORR-01 + 5 条 CORR-03）；cargo test 全部通过。
</objective>

<execution_context>
@/Users/guang/.claude/get-shit-done/workflows/execute-plan.md
@/Users/guang/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/02-correctness/02-RESEARCH.md
</context>

<interfaces>
<!-- 执行者需要的关键接口，从 codebase 提取，无需探索 -->

From src/parser.rs (from_path, lines 33-46):
```rust
pub fn from_path<P: AsRef<Path>>(path: P) -> Result<Self, ParseError> {
    let file = File::open(path).map_err(|e| ParseError::IoError(e.to_string()))?;
    let mmap = unsafe { Mmap::map(&file).map_err(|e| ParseError::IoError(e.to_string()))? };

    // Sample the first 64 KB to determine encoding.
    let sample = &mmap[..mmap.len().min(65536)];  // <-- CORR-01: 改为 &mmap[..]
    let encoding = if simd_from_utf8(sample).is_ok() {
        FileEncodingHint::Utf8
    } else {
        FileEncodingHint::Gb18030
    };

    Ok(Self { mmap, encoding })
}
```

From src/sqllog.rs (find_indicators_split, lines 212-230):
```rust
fn find_indicators_split(&self) -> usize {
    let data = &self.content_raw;
    let len = data.len();
    let start = len.saturating_sub(256);
    let window = &data[start..];

    let mut earliest = window.len();
    for finder in [
        &*FINDER_REV_EXECTIME,
        &*FINDER_REV_ROWCOUNT,
        &*FINDER_REV_EXEC_ID,
    ] {
        if let Some(idx) = finder.rfind(window) {
            earliest = earliest.min(idx);
        }
    }
    start + earliest  // <-- CORR-03: 在此处加验证守卫
}
```

Module-level function already in sqllog.rs:
```rust
fn parse_indicators_from_bytes(bytes: &[u8]) -> Option<PerformanceIndicators>
```
</interfaces>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: CORR-01 — 编码检测扩展至全文件 + 回归测试</name>
  <files>src/parser.rs, tests/sqllog_additional.rs</files>
  <read_first>
    - src/parser.rs（读取 from_path 方法，lines 33-46）
    - tests/sqllog_additional.rs（读取现有 GB18030 测试，找到文件末尾追加位置）
  </read_first>
  <behavior>
    - 新测试 `encoding_detection_gb18030_after_64kb_boundary`：
      - 用 ASCII 记录填充超过 65536 字节
      - 在末尾追加含 GB18030 编码用户名（"用户"）的记录
      - 断言最后一条记录的 `parse_meta().username == "用户"`
    - 修复后：`simd_from_utf8(&mmap[..])` 扫描整个文件，发现 GB18030 字节并返回 `Err`，编码设为 `Gb18030`
    - 现有所有测试仍通过（特别是 `file_encoding_detection_utf8`）
  </behavior>
  <action>
**步骤 1 — 写测试（RED）**

在 `tests/sqllog_additional.rs` 末尾追加：

```rust
#[test]
#[cfg(not(miri))]
fn encoding_detection_gb18030_after_64kb_boundary() {
    use encoding::all::GB18030;
    use encoding::{EncoderTrap, Encoding};
    use std::io::Write;
    use tempfile::NamedTempFile;

    // 用 ASCII 记录填充超过 64 KB
    let ascii_record = "2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:ascii trxid:0 stmt:0 appname:app) SELECT 1;\n";
    let repeat_count = 65536 / ascii_record.len() + 2;

    let username = "用户";
    let user_bytes = GB18030.encode(username, EncoderTrap::Strict).unwrap();
    let mut gb_line: Vec<u8> = b"2025-11-17 16:09:42.000 (EP[0] sess:2 thrd:2 user:".to_vec();
    gb_line.extend_from_slice(&user_bytes);
    gb_line.extend_from_slice(b" trxid:0 stmt:0 appname:app) SELECT 2;\n");

    let mut tmp = NamedTempFile::new().unwrap();
    for _ in 0..repeat_count {
        tmp.write_all(ascii_record.as_bytes()).unwrap();
    }
    tmp.write_all(&gb_line).unwrap();
    tmp.as_file().sync_all().unwrap();

    let parser = LogParser::from_path(tmp.path()).unwrap();
    let records: Vec<_> = parser.iter().collect();
    let last = records.last().unwrap().as_ref().unwrap();
    assert_eq!(last.parse_meta().username, username);
}
```

**步骤 2 — 验证测试失败（RED）**

运行 `cargo test encoding_detection_gb18030_after_64kb_boundary -- --nocapture`
预期：username 返回乱码或 panic，不等于 "用户"。

**步骤 3 — 修复（GREEN）**

在 `src/parser.rs` 第 37-38 行，将：
```rust
// Sample the first 64 KB to determine encoding.
let sample = &mmap[..mmap.len().min(65536)];
```
改为：
```rust
// Scan entire file to eliminate misclassification from early-section sampling.
let sample = &mmap[..];
```

注意：注释一并更新，不留旧注释。

**步骤 4 — 验证测试通过（GREEN）**

运行 `cargo test encoding_detection_gb18030_after_64kb_boundary -- --nocapture`
预期：PASS。
  </action>
  <verify>
    <automated>cargo test encoding_detection_gb18030_after_64kb_boundary -- --nocapture</automated>
  </verify>
  <done>
    - src/parser.rs line 38: `let sample = &mmap[..];`（不含 `.min(65536)`）
    - tests/sqllog_additional.rs 含 `encoding_detection_gb18030_after_64kb_boundary` 且标注 `#[cfg(not(miri))]`
    - `cargo test` 全部通过（包括现有 GB18030/UTF-8 测试）
  </done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: CORR-03 — find_indicators_split 验证守卫 + 五条假阳性测试</name>
  <files>src/sqllog.rs, tests/sqllog_additional.rs</files>
  <read_first>
    - src/sqllog.rs（完整读取 find_indicators_split 方法，lines 212-230；找到 parse_indicators_from_bytes 函数签名）
    - tests/sqllog_additional.rs（在 Task 1 追加后的状态，找到继续追加的位置）
    - tests/performance_metrics.rs（参考 parse_record 的使用方式，确认测试构造风格）
  </read_first>
  <behavior>
    测试 1 — EXECTIME: 在 SQL body，无真实指标：
    - 输入：SQL body = `"SELECT * FROM t WHERE col = 'EXECTIME: slow'"`
    - 期望：`body()` 返回完整字符串；`parse_indicators()` 返回 None

    测试 2 — ROWCOUNT: 在 SQL body，无真实指标：
    - 输入：SQL body = `"SELECT * FROM t WHERE cnt = 'ROWCOUNT: many'"`
    - 期望：`body()` 完整；`parse_indicators()` 返回 None

    测试 3 — EXEC_ID: 在 SQL body，无真实指标：
    - 输入：SQL body = `"SELECT EXEC_ID: foo FROM dual"`
    - 期望：`body()` 完整；`parse_indicators()` 返回 None

    测试 4 — SQL body 含伪指标关键字 + 真实指标存在：
    - 输入：SQL body 含 `EXECTIME: slow`，末尾有真实 `EXECTIME: 5.0(ms) ROWCOUNT: 1(rows) EXEC_ID: 99.`
    - 期望：`parse_indicators()` 返回 Some；`body()` 包含 SQL 主体（含伪指标部分）

    测试 5 — SQL body 含多个伪指标关键字，无真实指标：
    - 输入：SQL body = `"EXECTIME: x ROWCOUNT: y EXEC_ID: z"`（均为非数字格式）
    - 期望：`body()` 完整；`parse_indicators()` 返回 None
  </behavior>
  <action>
**步骤 1 — 写五条测试（RED）**

使用 `parse_record` 构造测试记录（无 mmap，Miri 兼容）。参考 performance_metrics.rs 中的构造模式：

```
"2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:u trxid:0 stmt:0 appname:a) {SQL_BODY}"
```

在 `tests/sqllog_additional.rs` 末尾追加（使用 `use dm_database_parser_sqllog::parser::parse_record;` 或已有 import）：

```rust
#[test]
fn find_indicators_split_exectime_keyword_in_sql_body_no_indicators() {
    let raw = "2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:u trxid:0 stmt:0 appname:a) SELECT * FROM t WHERE col = 'EXECTIME: slow'\n";
    let record = parse_record(raw.as_bytes(), Default::default()).unwrap();
    assert_eq!(record.body(), "SELECT * FROM t WHERE col = 'EXECTIME: slow'\n");
    assert!(record.parse_indicators().is_none());
}

#[test]
fn find_indicators_split_rowcount_keyword_in_sql_body_no_indicators() {
    let raw = "2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:u trxid:0 stmt:0 appname:a) SELECT * FROM t WHERE cnt = 'ROWCOUNT: many'\n";
    let record = parse_record(raw.as_bytes(), Default::default()).unwrap();
    assert_eq!(record.body(), "SELECT * FROM t WHERE cnt = 'ROWCOUNT: many'\n");
    assert!(record.parse_indicators().is_none());
}

#[test]
fn find_indicators_split_exec_id_keyword_in_sql_body_no_indicators() {
    let raw = "2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:u trxid:0 stmt:0 appname:a) SELECT EXEC_ID: foo FROM dual\n";
    let record = parse_record(raw.as_bytes(), Default::default()).unwrap();
    assert_eq!(record.body(), "SELECT EXEC_ID: foo FROM dual\n");
    assert!(record.parse_indicators().is_none());
}

#[test]
fn find_indicators_split_keyword_in_body_plus_real_indicators() {
    // SQL body 含伪关键字，但末尾有真实指标
    let raw = "2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:u trxid:0 stmt:0 appname:a) SELECT EXECTIME: slow\nEXECTIME: 5.0(ms) ROWCOUNT: 1(rows) EXEC_ID: 99.\n";
    let record = parse_record(raw.as_bytes(), Default::default()).unwrap();
    // 真实指标存在，parse_indicators() 应返回 Some
    assert!(record.parse_indicators().is_some());
    // body 包含 SQL 主体部分
    assert!(record.body().contains("SELECT"));
}

#[test]
fn find_indicators_split_multiple_keywords_in_body_no_indicators() {
    let raw = "2025-11-17 16:09:41.123 (EP[0] sess:1 thrd:2 user:u trxid:0 stmt:0 appname:a) EXECTIME: x ROWCOUNT: y EXEC_ID: z\n";
    let record = parse_record(raw.as_bytes(), Default::default()).unwrap();
    assert_eq!(record.body(), "EXECTIME: x ROWCOUNT: y EXEC_ID: z\n");
    assert!(record.parse_indicators().is_none());
}
```

**步骤 2 — 验证测试失败（RED）**

运行 `cargo test find_indicators_split -- --nocapture`
预期：测试 1/2/3/5 因 body() 被截断而 FAIL；测试 4 可能通过。

**步骤 3 — 修复（GREEN）**

在 `src/sqllog.rs` 的 `find_indicators_split` 方法中（lines 212-230），将：
```rust
    start + earliest
}
```
替换为：
```rust
    let split = start + earliest;
    // Only accept the split if the trailing slice contains parseable indicators.
    // If parse_indicators_from_bytes returns None, the "indicator-looking" text
    // is part of the SQL body — return len (no split).
    if split < len && parse_indicators_from_bytes(&data[split..]).is_none() {
        return len;
    }
    split
}
```

完整修改后的函数体（验证不超过 40 行）：
```rust
fn find_indicators_split(&self) -> usize {
    let data = &self.content_raw;
    let len = data.len();
    let start = len.saturating_sub(256);
    let window = &data[start..];

    // Use SIMD reverse finders; take the leftmost (minimum) match position.
    let mut earliest = window.len();
    for finder in [
        &*FINDER_REV_EXECTIME,
        &*FINDER_REV_ROWCOUNT,
        &*FINDER_REV_EXEC_ID,
    ] {
        if let Some(idx) = finder.rfind(window) {
            earliest = earliest.min(idx);
        }
    }

    let split = start + earliest;
    // Only accept the split if the trailing slice contains parseable indicators.
    if split < len && parse_indicators_from_bytes(&data[split..]).is_none() {
        return len;
    }
    split
}
```

**步骤 4 — 验证（GREEN）**

运行 `cargo test find_indicators_split -- --nocapture`
预期：全部 5 条测试 PASS。

然后运行完整测试套件验证无回归。
  </action>
  <verify>
    <automated>cargo test find_indicators_split -- --nocapture && cargo test</automated>
  </verify>
  <done>
    - src/sqllog.rs::find_indicators_split 末尾有 `parse_indicators_from_bytes(&data[split..]).is_none()` 守卫
    - tests/sqllog_additional.rs 含 5 条以 `find_indicators_split_` 开头的测试，全部 PASS
    - `cargo test` 全部通过（无回归）
    - 函数体不超过 40 行
  </done>
</task>

</tasks>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| 文件系统 → mmap | 文件内容不受信任；编码检测是唯一的 safety 关卡 |
| 用户输入 → SQL body | SQL body 内容任意；find_indicators_split 不应对其做不安全假设 |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-02-01 | Tampering | `from_path` 编码检测 | mitigate | CORR-01 修复：全文件扫描，消除截断误判导致的 UB |
| T-02-02 | Information Disclosure | `find_indicators_split` 错误切分 | mitigate | CORR-03 修复：验证守卫确保只在真实指标处切分 |
| T-02-03 | Denial of Service | 超大文件全文扫描 | accept | simdutf8 ~50 GB/s，单次 one-time 开销可接受；无循环 |
</threat_model>

<verification>
1. `cargo test` — 全部测试通过，无 FAIL/SKIP 意外
2. `cargo clippy` — 无新警告
3. `grep -n 'mmap.len().min(65536)' src/parser.rs` — 无输出（确认旧代码已删除）
4. `grep -n 'parse_indicators_from_bytes' src/sqllog.rs` — 至少 2 处（原有调用 + 新守卫）
5. `cargo test encoding_detection_gb18030_after_64kb_boundary -- --nocapture` — PASS
6. `cargo test find_indicators_split -- --nocapture` — 5 条全 PASS
</verification>

<success_criteria>
- CORR-01: src/parser.rs 第 38 行为 `let sample = &mmap[..];`，不含 `.min(65536)`
- CORR-03: src/sqllog.rs 的 find_indicators_split 含验证守卫，5 条新测试全部通过
- cargo test 0 failures
- cargo clippy 0 new warnings
- 覆盖率 ≥90%（已有基础，新测试只增不减）
</success_criteria>

<output>
完成后在 `.planning/phases/02-correctness/02-01-SUMMARY.md` 记录：
- 修改的文件和具体行号
- 新增测试名称列表
- cargo test 输出摘要（pass/fail 计数）
- 任何与研究文档不符的发现
</output>
