---
phase: 25.1-parser-robustness-security-hardening
plan: "01"
type: tdd
wave: 1
depends_on: []
files_modified:
  - tests/parse_proptest.rs
  - fuzz/Cargo.toml
  - fuzz/fuzz_targets/fuzz_ddl_parse.rs
  - fuzz/corpus/fuzz_ddl_parse/seed_basic.txt
  - fuzz/corpus/fuzz_ddl_parse/seed_tab_ws.txt
  - fuzz/corpus/fuzz_ddl_parse/seed_null_byte.txt
  - fuzz/corpus/fuzz_ddl_parse/seed_long.txt
  - fuzz/corpus/fuzz_ddl_parse/seed_homoglyph.txt
autonomous: true
requirements: []

must_haves:
  truths:
    - "cargo test fails on the new whitespace proptest (RED state confirmed)"
    - "cargo test fails on the adversarial unit tests (RED state confirmed)"
    - "fuzz target fuzz_ddl_parse compiles under nightly"
    - "fuzz corpus seeds exist and are non-empty"
  artifacts:
    - path: "tests/parse_proptest.rs"
      provides: "TEST-07 proptest block + TEST-08 adversarial unit tests (failing)"
      contains: "arb_inter_keyword_ws"
    - path: "fuzz/fuzz_targets/fuzz_ddl_parse.rs"
      provides: "libFuzzer target exercising detect_semantic_view_ddl + validate_and_rewrite"
      exports: []
    - path: "fuzz/Cargo.toml"
      provides: "fuzz_ddl_parse binary registration"
      contains: "fuzz_ddl_parse"
    - path: "fuzz/corpus/fuzz_ddl_parse/seed_basic.txt"
      provides: "corpus seed: basic valid DDL"
  key_links:
    - from: "tests/parse_proptest.rs"
      to: "src/parse.rs"
      via: "use semantic_views::parse::*"
      pattern: "detect_ddl_kind"
    - from: "fuzz/fuzz_targets/fuzz_ddl_parse.rs"
      to: "src/parse.rs"
      via: "semantic_views::parse::{detect_semantic_view_ddl, validate_and_rewrite}"
      pattern: "validate_and_rewrite"
---

<objective>
Write the failing tests and fuzz harness that define the RED state for Phase 25.1.

Purpose: Establish concrete, executable test expectations before the production code fix lands. The tests must fail against the current `detect_ddl_kind` (which uses literal byte prefix matching) and pass after Plan 02 introduces token-based matching. The fuzz target must compile and be registerable — it does not need to find bugs yet.

Output:
- TEST-07 proptest block in `tests/parse_proptest.rs` with `arb_inter_keyword_ws` strategy covering all 7 DDL forms with inter-keyword whitespace variants
- TEST-08 `#[test]` block with adversarial unit tests (very long string, null byte, semicolons in name, Unicode homoglyphs, control characters)
- `fuzz/fuzz_targets/fuzz_ddl_parse.rs` exercising the full detect -> validate_and_rewrite pipeline on arbitrary byte sequences
- 5 corpus seeds in `fuzz/corpus/fuzz_ddl_parse/`
- `fuzz/Cargo.toml` updated to register the new binary
</objective>

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

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/phases/25.1-parser-robustness-security-hardening/25.1-RESEARCH.md
@.planning/phases/25.1-parser-robustness-security-hardening/25.1-VALIDATION.md

<interfaces>
<!-- Key types from src/parse.rs that the tests call -->
```rust
pub const PARSE_NOT_OURS: u8 = 0;
pub const PARSE_DETECTED: u8 = 1;

pub enum DdlKind { Create, CreateOrReplace, CreateIfNotExists, Drop, DropIfExists, Describe, Show }

pub fn detect_ddl_kind(query: &str) -> Option<DdlKind>;
pub fn detect_semantic_view_ddl(query: &str) -> u8;
pub fn validate_and_rewrite(query: &str) -> Result<Option<String>, ParseError>;

pub struct ParseError { pub message: String, pub position: Option<usize> }
```

<!-- Existing proptest helpers in tests/parse_proptest.rs -->
```rust
fn arb_whitespace() -> impl Strategy<Value = String>        // [ \t]{0,10}
fn arb_view_name() -> impl Strategy<Value = String>         // [a-z_][a-z0-9_]{0,29}
fn arb_semicolons() -> impl Strategy<Value = String>        // ;{0,3}
fn arb_case_variant(prefix: &'static str) -> impl Strategy<Value = String>
fn build_suffix(kind: DdlKind, name: &str) -> String
fn build_as_body_suffix(name: &str) -> String

const DDL_FORMS: &[(&str, DdlKind)]    // 7 entries
const CREATE_FORMS: &[(&str, DdlKind, &str)]   // 3 entries
const NAME_ONLY_FORMS: &[(&str, DdlKind, &str)] // 3 entries
```

<!-- Existing fuzz target pattern (fuzz/fuzz_targets/fuzz_query_names.rs) -->
```rust
#![no_main]
use libfuzzer_sys::fuzz_target;
fuzz_target!(|input: SomeType| { ... });
```

<!-- fuzz/Cargo.toml registration pattern -->
```toml
[[bin]]
name = "fuzz_ddl_parse"
path = "fuzz_targets/fuzz_ddl_parse.rs"
doc = false
```
</interfaces>
</context>

<feature>
  <name>Failing test suite and fuzz target for Phase 25.1 token-based detection</name>
  <files>tests/parse_proptest.rs, fuzz/Cargo.toml, fuzz/fuzz_targets/fuzz_ddl_parse.rs, fuzz/corpus/fuzz_ddl_parse/ (5 seed files)</files>
  <behavior>
    TEST-07 (proptest): For each of the 7 DDL forms, replacing single spaces between keywords with tabs, newlines, carriage returns, or mixed whitespace must produce the correct DdlKind. These tests MUST fail with the current starts_with_ci implementation and MUST pass after Plan 02's token-based fix.

    TEST-08 (unit tests):
    - 1 MB string of 'A' chars returns PARSE_NOT_OURS from detect_semantic_view_ddl in under 1 ms (no hang)
    - detect_ddl_kind on "CREATE SEMANTIC VIEW" returns Some(DdlKind::Create) (sanity)
    - validate_and_rewrite("CREATE SEMANTIC VIEW x\0bad (tables := [], dimensions := [])") does not panic; returns Ok or Err, never panics
    - validate_and_rewrite("CREATE SEMANTIC VIEW x;injection (tables := [], dimensions := [])") produces safe output — the view name stops at ';' (name_end scan) or returns an error; the output SQL must not contain a raw unescaped ';' that breaks the SELECT wrapper
    - detect_ddl_kind("СREATE SEMANTIC VIEW x") (Cyrillic С) returns None — no near-miss confusion
    - validate_and_rewrite("CREATE SEMANTIC VIEW x\x01bad (tables := [], dimensions := [])") does not panic

    Fuzz target:
    - Accepts raw &[u8], attempts std::str::from_utf8 — returns early if invalid UTF-8 (not a panic)
    - Calls detect_semantic_view_ddl(query)
    - If PARSE_DETECTED, calls validate_and_rewrite(query) — asserts no panic
    - If Ok(Some(sql)), asserts sql starts with "SELECT * FROM "

    RED STATE CONFIRMATION: After writing tests but before Plan 02 lands, run `cargo test -- test_prefix_whitespace` or any TEST-07 proptest name and confirm failure output. Document the failing test names in the task summary.
  </behavior>
  <implementation>
    In tests/parse_proptest.rs, append after the existing TEST-06 block:

    // TEST-07: Prefix whitespace variants
    Add a new proptest! block named "prefix_whitespace" containing:
    - Strategy `arb_inter_keyword_ws()` generating `[ \t\n\r]{1,4}` (NOT the existing arb_whitespace which only covers leading space/tab)
    - For each DDL_FORMS entry (form_idx in 0..7), replace each single space in the prefix with the generated whitespace separator, build the full query with build_suffix, call detect_ddl_kind, assert Some(expected_kind)
    - The prefix replacement must split the prefix on ASCII spaces and rejoin with the generated separator — e.g., "create semantic view" split → ["create", "semantic", "view"] rejoined with sep → "create\tsemantic\tview"

    // TEST-08: Adversarial inputs
    Add a separate #[cfg(test)] module or plain #[test] functions (not proptest blocks):
    - fn test_large_input_no_panic — 1 MB of 'A' → PARSE_NOT_OURS, uses std::time::Instant to assert < 50ms
    - fn test_null_byte_in_name — catch_unwind wrapping validate_and_rewrite, assert no panic
    - fn test_semicolon_in_name — validate output SQL is valid (starts_with "SELECT * FROM ") or is Err; must not contain "; DROP"
    - fn test_unicode_homoglyph — Cyrillic С prefix returns None from detect_ddl_kind
    - fn test_control_char_in_name — \x01 in view name does not panic

    In fuzz/fuzz_targets/fuzz_ddl_parse.rs:
    - Use raw &[u8] input (not Arbitrary struct) via fuzz_target!(|data: &[u8]| { ... })
    - from_utf8 check: if invalid, return immediately
    - Call detect_semantic_view_ddl and validate_and_rewrite per the VALIDATION.md fuzz target design
    - Assert output sql.starts_with("SELECT * FROM ") when Some

    In fuzz/Cargo.toml:
    - Append [[bin]] entry for fuzz_ddl_parse (no new dependencies needed — semantic_views is already imported)

    Create fuzz/corpus/fuzz_ddl_parse/ directory with 5 seed files:
    - seed_basic.txt: `CREATE SEMANTIC VIEW v (tables := [], dimensions := [])`
    - seed_tab_ws.txt: `CREATE\tSEMANTIC\tVIEW\tv (tables := [], dimensions := [])`
    - seed_null_byte.txt: `CREATE SEMANTIC VIEW v\x00bad (tables := [], dimensions := [])`
    - seed_long.txt: `CREATE SEMANTIC VIEW ` followed by 200 'A' chars + ` (tables := [], dimensions := [])`
    - seed_homoglyph.txt: `СREATE SEMANTIC VIEW v (tables := [], dimensions := [])` (Cyrillic С)
  </implementation>
</feature>

<tasks>

<task type="auto" tdd="true">
  <name>Task 1: Write TEST-07 failing proptest (prefix whitespace variants)</name>
  <files>tests/parse_proptest.rs</files>
  <behavior>
    - detect_ddl_kind("create\tsemantic\tview v (...)") returns Some(DdlKind::Create) — currently FAILS
    - detect_ddl_kind("create\nsemantic\nview v (...)") returns Some(DdlKind::Create) — currently FAILS
    - detect_ddl_kind("CREATE\r\nSEMANTIC\r\nVIEW v (...)") returns Some(DdlKind::Create) — currently FAILS
    - Same for all 7 DDL forms with mixed inter-keyword whitespace
    - detect_ddl_kind("create  semantic  view v (...)") (double-space) returns Some(DdlKind::Create) — currently FAILS
  </behavior>
  <action>
    Append a new proptest! block after the TEST-06 block in tests/parse_proptest.rs.

    New strategy `arb_inter_keyword_ws()`:
    ```
    fn arb_inter_keyword_ws() -> impl Strategy<Value = String> {
        proptest::string::string_regex("[ \t\n\r]{1,4}").unwrap()
    }
    ```

    New proptest! block comment: `// TEST-07: Prefix inter-keyword whitespace variants`

    Two properties in the block:

    1. `prefix_whitespace_detection` — for all 7 DDL forms (form_idx 0..7), with arb_inter_keyword_ws and arb_view_name:
       - Split the form prefix on ' ' into keywords, rejoin with the generated separator
       - Append build_suffix(kind, &name) after the rejoined prefix
       - Assert detect_ddl_kind(&query) == Some(expected_kind)
       - IMPORTANT: For SHOW ("show semantic views"), build_suffix returns "" — append nothing

    2. `prefix_whitespace_rewrite_roundtrip` — for the 3 CREATE forms (form_idx 0..3usize), with arb_inter_keyword_ws and arb_view_name:
       - Build the same prefix with non-space separator
       - Append a valid paren body suffix: ` {name} (tables := ['t'], dimensions := ['d'])`
       - Call validate_and_rewrite(&query)
       - Assert Ok(Some(sql)) where sql starts with "SELECT * FROM "
       - This verifies the full rewrite pipeline handles whitespace-variant prefixes

    Run `cargo test prefix_whitespace` after writing — confirm FAILURE (RED state). Record exact failure message in task summary.
  </action>
  <verify>
    <automated>cargo test prefix_whitespace 2>&1 | grep -E "FAILED|test result"</automated>
  </verify>
  <done>Tests exist in parse_proptest.rs AND fail against current implementation (RED confirmed in output)</done>
</task>

<task type="auto" tdd="true">
  <name>Task 2: Write TEST-08 adversarial unit tests + fuzz target</name>
  <files>tests/parse_proptest.rs, fuzz/Cargo.toml, fuzz/fuzz_targets/fuzz_ddl_parse.rs, fuzz/corpus/fuzz_ddl_parse/seed_basic.txt, fuzz/corpus/fuzz_ddl_parse/seed_tab_ws.txt, fuzz/corpus/fuzz_ddl_parse/seed_null_byte.txt, fuzz/corpus/fuzz_ddl_parse/seed_long.txt, fuzz/corpus/fuzz_ddl_parse/seed_homoglyph.txt</files>
  <behavior>
    - test_large_input_no_panic: 1 MB of 'A' returns PARSE_NOT_OURS, completes in < 50ms
    - test_null_byte_in_name: validate_and_rewrite does not panic; returns Ok or Err
    - test_semicolon_in_name: rewritten SQL starts with "SELECT * FROM " (name extraction stops before ';')
    - test_unicode_homoglyph: detect_ddl_kind("СREATE SEMANTIC VIEW x") returns None
    - test_control_char_in_name: validate_and_rewrite does not panic
    - fuzz target compiles: `cargo +nightly fuzz build fuzz_ddl_parse` succeeds
  </behavior>
  <action>
    **Part A: TEST-08 in tests/parse_proptest.rs**

    Append a `// TEST-08: Adversarial inputs` comment block with plain `#[test]` functions (not proptest):

    ```rust
    // TEST-08: Adversarial inputs
    // These tests document safe behavior for inputs that are not expected in normal
    // DuckDB usage but must not cause panics or exploitable behavior.

    #[test]
    fn test_large_input_no_panic() {
        let s: String = std::iter::repeat('A').take(1_000_000).collect();
        let start = std::time::Instant::now();
        let result = detect_semantic_view_ddl(&s);
        let elapsed = start.elapsed();
        assert_eq!(result, PARSE_NOT_OURS, "1MB non-DDL should return PARSE_NOT_OURS");
        assert!(elapsed.as_millis() < 50, "1MB scan took {}ms, expected <50ms", elapsed.as_millis());
    }

    #[test]
    fn test_null_byte_in_name() {
        // Null bytes are valid UTF-8 in Rust but may truncate C strings at the FFI boundary.
        // The parser must not panic. It may return Ok (if null byte in name is tolerated)
        // or Err (if the name validator rejects it). Either is acceptable.
        let query = "CREATE SEMANTIC VIEW x\x00bad (tables := [], dimensions := [])";
        let result = std::panic::catch_unwind(|| validate_and_rewrite(query));
        assert!(result.is_ok(), "validate_and_rewrite panicked on null byte in name");
    }

    #[test]
    fn test_semicolon_in_name() {
        // Name extraction stops at whitespace or '(' — a ';' inside the "name" position
        // means the name is everything before ';' or the whole token. Either way, the
        // rewritten SQL must start with "SELECT * FROM " (no raw ';' injected into the wrapper).
        let query = "CREATE SEMANTIC VIEW x;injection (tables := [], dimensions := [])";
        match validate_and_rewrite(query) {
            Ok(Some(sql)) => {
                assert!(sql.starts_with("SELECT * FROM "),
                    "Rewritten SQL must start with SELECT * FROM, got: {sql}");
                // The semicolon must not appear outside a SQL string literal in a way
                // that could terminate the wrapper SELECT statement.
                // Since the name ends at whitespace/'(', the name is "x;injection" or
                // the parser hits an error. Either outcome is safe.
            }
            Ok(None) => {} // Not detected as our DDL — acceptable
            Err(_) => {}   // Parse error — acceptable
        }
    }

    #[test]
    fn test_unicode_homoglyph() {
        // Cyrillic 'С' (U+0421) looks like Latin 'C' but is a different byte sequence.
        // The ASCII-only keyword matcher must return None — no near-miss confusion.
        // Note: DuckDB may not pass Unicode-prefixed queries to extension hooks at all
        // (StripUnicodeSpaces normalisation), but the pure-Rust parser must be safe.
        let query = "СREATE SEMANTIC VIEW x (tables := [], dimensions := [])";
        assert_eq!(detect_ddl_kind(query), None,
            "Cyrillic С prefix should not match as DdlKind");
        assert_eq!(detect_semantic_view_ddl(query), PARSE_NOT_OURS,
            "Cyrillic С prefix should return PARSE_NOT_OURS");
    }

    #[test]
    fn test_control_char_in_name() {
        // Control characters (0x01-0x1F except whitespace) in view names must not panic.
        let query = "CREATE SEMANTIC VIEW x\x01bad (tables := [], dimensions := [])";
        let result = std::panic::catch_unwind(|| validate_and_rewrite(query));
        assert!(result.is_ok(), "validate_and_rewrite panicked on control char in name");
    }
    ```

    **Part B: fuzz/fuzz_targets/fuzz_ddl_parse.rs**

    Create new file:
    ```rust
    #![no_main]
    use libfuzzer_sys::fuzz_target;
    use semantic_views::parse::{detect_semantic_view_ddl, validate_and_rewrite, PARSE_DETECTED};

    fuzz_target!(|data: &[u8]| {
        // Reject invalid UTF-8 — not a crash, just out of scope for this target.
        let Ok(query) = std::str::from_utf8(data) else {
            return;
        };

        // detect_semantic_view_ddl must never panic regardless of input.
        let detected = detect_semantic_view_ddl(query);

        // If detected as our DDL, validate_and_rewrite must also never panic.
        if detected == PARSE_DETECTED {
            match validate_and_rewrite(query) {
                Ok(Some(sql)) => {
                    // Rewritten SQL must start with the expected prefix.
                    assert!(
                        sql.starts_with("SELECT * FROM "),
                        "Rewritten SQL does not start with 'SELECT * FROM ': {sql}"
                    );
                }
                Ok(None) => {
                    // Detection/rewrite disagreement — not a panic, document if seen.
                }
                Err(_) => {
                    // Parse error — acceptable, not a panic.
                }
            }
        }
    });
    ```

    **Part C: Register in fuzz/Cargo.toml**

    Append after the existing [[bin]] entries:
    ```toml
    [[bin]]
    name = "fuzz_ddl_parse"
    path = "fuzz_targets/fuzz_ddl_parse.rs"
    doc = false
    ```

    **Part D: Create corpus seeds**

    Create directory fuzz/corpus/fuzz_ddl_parse/ and write 5 seed files with exact byte content:
    - seed_basic.txt: `CREATE SEMANTIC VIEW v (tables := [], dimensions := [])`
    - seed_tab_ws.txt: content with literal tab characters between CREATE, SEMANTIC, VIEW (tab = \x09)
    - seed_null_byte.txt: `CREATE SEMANTIC VIEW v` + null byte (0x00) + `bad (tables := [], dimensions := [])`
    - seed_long.txt: `CREATE SEMANTIC VIEW ` + 200 'A' characters + ` (tables := [], dimensions := [])`
    - seed_homoglyph.txt: UTF-8 bytes for "СREATE SEMANTIC VIEW v" (Cyrillic С = 0xD0 0xA1)

    **Verification step:** Run `cargo test test_large_input_no_panic test_null_byte_in_name test_semicolon_in_name test_unicode_homoglyph test_control_char_in_name` — these should PASS (they test safety invariants that hold before and after the fix). Run `cargo test prefix_whitespace` — this should still FAIL (RED state preserved until Plan 02).

    Note: `cargo +nightly fuzz build fuzz_ddl_parse` verifies the fuzz target compiles — run this if nightly toolchain is available, skip if not installed (the fuzz target correctness is verified by code review instead).
  </action>
  <verify>
    <automated>cargo test test_large_input_no_panic test_null_byte_in_name test_semicolon_in_name test_unicode_homoglyph test_control_char_in_name 2>&1 | tail -5</automated>
  </verify>
  <done>All 5 adversarial unit tests PASS. fuzz_ddl_parse.rs exists with correct structure. fuzz/Cargo.toml has the [[bin]] entry. 5 corpus seeds exist in fuzz/corpus/fuzz_ddl_parse/. cargo test prefix_whitespace still FAILS (RED state intact).</done>
</task>

</tasks>

<verification>
After Plan 01:
- `cargo test test_large_input` — PASS
- `cargo test prefix_whitespace` — FAIL (RED, expected)
- fuzz/fuzz_targets/fuzz_ddl_parse.rs exists and has correct structure
- fuzz/Cargo.toml contains fuzz_ddl_parse [[bin]] entry
- fuzz/corpus/fuzz_ddl_parse/ contains 5 non-empty seed files
</verification>

<success_criteria>
1. tests/parse_proptest.rs contains TEST-07 proptest block with arb_inter_keyword_ws covering all 7 DDL forms (currently RED)
2. tests/parse_proptest.rs contains TEST-08 block with 5 adversarial unit tests (currently PASSING — invariants hold before and after fix)
3. fuzz/fuzz_targets/fuzz_ddl_parse.rs is a valid libFuzzer target that accepts &[u8] and tests both detect and validate_and_rewrite
4. fuzz/corpus/fuzz_ddl_parse/ contains 5 seed files (at least seed_basic.txt, seed_tab_ws.txt, seed_null_byte.txt, seed_long.txt, seed_homoglyph.txt)
5. fuzz/Cargo.toml registers fuzz_ddl_parse as a [[bin]] target
</success_criteria>

<output>
After completion, create `.planning/phases/25.1-parser-robustness-security-hardening/25.1-01-SUMMARY.md`
</output>
