dsct 0.2.2

LLM-friendly packet dissector CLI
Documentation
# 設計案: DissectBuffer の再利用

## 背景

`DissectBuffer::new()` はパケットごとに 4 つの `Vec` をヒープ確保する:

```rust
pub fn new() -> Self {
    Self {
        layers: Vec::with_capacity(8),
        fields: Vec::with_capacity(64),
        scratch: Vec::with_capacity(256),
        aux_data_len: 0,
        aux_chunks: Vec::new(),
    }
}
```

`clear()` を使えばキャパシティを保持したまま内容だけリセットできるが、現在の
API 構造ではライフタイム制約により再利用が不可能。

## 問題の根本原因

### FieldValue のゼロコピー借用

`FieldValue<'pkt>` は `Bytes(&'pkt [u8])` と `Str(&'pkt str)` でパケット
データを直接参照する。この `'pkt` が `DissectBuffer<'pkt>` 全体に伝播する:

```rust
pub struct DissectBuffer<'pkt> {
    fields: Vec<Field<'pkt>>,  // ← 'pkt がここに現れる
    // ...
}
```

### &mut DissectBuffer の不変性 (Invariance)

`dissect_with_link_type` のシグネチャ:

```rust
fn dissect_with_link_type<'pkt>(
    &self,
    data: &'pkt [u8],
    link_type: u32,
    buf: &mut DissectBuffer<'pkt>,
) -> Result<(), PacketError>
```

`&mut T<'a>` は `'a` に対して不変 (invariant)。つまり:
- `DissectBuffer<'long>``&mut DissectBuffer<'short>` として渡せない
- パケットデータ `data: &'pkt [u8]` のライフタイムと buf の `'pkt`**完全一致**が必要

### for_each_packet のクロージャスコープ

```rust
// stream_packets の内部:
loop {
    let pkt_data: &[u8] = read_next_packet(&mut internal_buf);
    //          ^^^^^ ライフタイムはこのループイテレーション内のみ
    f(&record, pkt_data);
    // ここで pkt_data が無効化 → DissectBuffer 内の参照も無効
}
```

クロージャ外の `DissectBuffer` はクロージャ内の `pkt_data` より長命なので、
不変性によりコンパイラが拒否する。

## 

### 案 A: `for_each_packet``DissectBuffer` を渡す API (推奨)

`CaptureReader` の API を拡張し、DissectBuffer の生成と再利用をコールバックの
外側で管理する。

```rust
// packet-dissector-pcap 側の変更
pub fn stream_packets_with_buf<R, F>(
    reader: R,
    buf: &mut DissectBuffer<'_>,  // 呼び出し側で生成
    f: F,
) -> Result<(), PcapError>
where
    R: Read,
    F: FnMut(&PacketRecord, &DissectBuffer<'_>) -> ControlFlow<()>,
{
    let mut pkt_buf = Vec::new();
    loop {
        let record = match read_record(&mut reader, &mut pkt_buf)? {
            Some(r) => r,
            None => break,
        };
        buf.clear();
        registry.dissect(&pkt_buf, buf)?;
        if f(&record, buf).is_break() {
            break;
        }
    }
}
```

**ポイント:**
- `DissectBuffer``pkt_data` が同じスコープ内で同一ライフタイムを持つ
- `clear()``dissect()` → コールバック呼び出しが同一スコープ内で完結
- コールバックは `&DissectBuffer` (不変参照) を受け取るだけ

**dsct 側の利用:**

```rust
// input.rs
pub fn for_each_dissected<F>(
    self,
    registry: &DissectorRegistry,
    mut f: F,
) -> Result<()>
where
    F: FnMut(PacketMeta, &DissectBuffer<'_>, &[u8]) -> Result<ControlFlow<()>>,
{
    let mut buf = DissectBuffer::new();
    let mut counter = 0u64;
    // ...
    stream_packets(self.reader, |record, pkt_data| {
        counter += 1;
        buf.clear();
        if let Err(e) = registry.dissect_with_link_type(
            pkt_data, record.link_type, &mut buf
        ) {
            // handle error
            return ControlFlow::Continue(());
        }
        let meta = PacketMeta { /* ... */ };
        match f(meta, &buf, pkt_data) {
            Ok(flow) => flow,
            Err(e) => { /* ... */ ControlFlow::Break(()) }
        }
    });
}
```

**トレードオフ:**
- ✅ ゼロコピーを維持
-`packet-dissector-pcap` 側の変更が小さい(新関数追加のみ)
-`dsct` 側は `for_each_dissected` に移行するだけ
- ⚠️ `stream_packets` の内部で dissect を呼ぶため、`DissectorRegistry`  pcap クレートに渡すか、dsct 側で `stream_packets` を直接使う必要がある

**推奨する実装方針:**

pcap クレートを変更せず、dsct の `for_each_packet` 内部で解決できる:

```rust
// input.rs — dsct 側のみの変更
pub fn for_each_packet<F>(self, mut f: F) -> Result<()>
where
    F: FnMut(PacketMeta, &[u8]) -> Result<ControlFlow<()>>,
{
    // 既存実装 (変更なし)
}

/// Dissect-aware iteration: reuses a single DissectBuffer across packets.
pub fn for_each_dissected<F>(
    self,
    registry: &DissectorRegistry,
    mut f: F,
) -> Result<()>
where
    F: for<'pkt> FnMut(
        PacketMeta,
        &Packet<'_, 'pkt>,
    ) -> Result<ControlFlow<()>>,
{
    let mut dissect_buf = DissectBuffer::new();
    let mut counter = 0u64;
    let mut error: Option<DsctError> = None;

    let stream_result =
        packet_dissector_pcap::stream_packets(self.reader, |record, pkt_data| {
            counter += 1;
            dissect_buf.clear();

            let meta = PacketMeta { /* ... */ };

            if let Err(e) = registry.dissect_with_link_type(
                pkt_data, record.link_type as u32, &mut dissect_buf
            ) {
                // 必要に応じてエラー処理
                return ControlFlow::Continue(());
            }
            let packet = Packet::new(&dissect_buf, pkt_data);

            match f(meta, &packet) {
                Ok(flow) => flow,
                Err(e) => {
                    error = Some(e);
                    ControlFlow::Break(())
                }
            }
        });
    // ...
}
```

**これが動作する理由:**

`stream_packets` に渡すクロージャ内部で `dissect_buf`, `pkt_data`,
`Packet` が全て同一スコープに存在する。`dissect_buf` はクロージャに
`&mut` でキャプチャされ、`pkt_data` はコールバック引数。Rust の
ライフタイム推論により:

1. `dissect_buf.clear()` で既存の借用を全て無効化
2. `dissect_with_link_type(pkt_data, ..., &mut dissect_buf)`   `dissect_buf``pkt_data` のライフタイムが結び付く
3. `Packet::new(&dissect_buf, pkt_data)` で読み取り専用ビューを作成
4. `f(meta, &packet)` でユーザーコールバックに渡す
5. コールバック戻り後、`packet` がドロップ → `dissect_buf` の借用解放
6. 次のイテレーションで `clear()` が呼べる

### 案 B: OwnedDissectBuffer (ゼロコピーを諦める)

`packet-dissector-core` に `'static` な所有型バッファを追加する:

```rust
pub struct OwnedDissectBuffer {
    layers: Vec<Layer>,
    fields: Vec<OwnedField>,      // Bytes/Str をコピー済み
    scratch: Vec<u8>,
    data: Vec<u8>,                 // パケットデータのコピー
}

pub enum OwnedFieldValue {
    U8(u8),
    U16(u16),
    // ...
    Bytes(Vec<u8>),               // 所有
    Str(String),                  // 所有
    // ...
}
```

**トレードオフ:**
- ✅ ライフタイム制約が完全に消える
- ✅ クロージャ外に持ち出せる
-`Bytes`/`Str` の毎パケットコピーコスト
-`packet-dissector-core` に大きな型追加が必要
- ❌ 既存 API との二重管理

**用途が限定的:** TUI の `OwnedPacket` が既にこのアプローチを独自実装している
(`src/tui/owned_packet.rs`)。汎用化する価値があるかは利用頻度次第。

### 案 C: mmap ベースのインデックスアクセス

`build_index` で全パケットのオフセットを取得し、mmap されたファイル全体から
各パケットをスライスする:

```rust
let data: &[u8] = mmap_file(path)?;
let index: Vec<PacketRecord> = build_index(data)?;
let mut buf = DissectBuffer::new();

for record in &index {
    let pkt_data = &data[record.data_offset..][..record.captured_len as usize];
    buf.clear();
    registry.dissect_with_link_type(pkt_data, record.link_type, &mut buf)?;
    let packet = Packet::new(&buf, pkt_data);
    // use packet...
}
```

**トレードオフ:**
- ✅ ゼロコピー + バッファ再利用の両立
-`packet-dissector` 側の変更不要
- ❌ mmap 必須 → stdin 入力 (`-`) に使えない
- ❌ 大きなファイルでインデックス構築のメモリコスト
- ❌ シーク可能なファイル限定

**用途:** TUI モードでは既に mmap + index を使っている。CLI の `read`/`stats`
にも `--file` 入力時のみ適用可能。

## 推奨

**案 A (`for_each_dissected`) を推奨。**

理由:
1. `packet-dissector` 側の変更なしで dsct 内だけで完結する
2. ゼロコピーを維持したままバッファ再利用を実現
3. stdin 入力もファイル入力も両方サポート
4. 既存の `for_each_packet` と共存可能(段階的移行)
5. ライフタイムの整合性が静的に保証される

## 実装手順

1. `src/input.rs``for_each_dissected` メソッドを追加
2. `cmd_read``for_each_dissected` に移行
3. `cmd_stats``for_each_dissected` に移行
4. ベンチマークで効果を測定 (`cargo bench`)
5. `for_each_packet` は MCP 等の他用途向けに残す