///|
/// 解析 IPv4 头(偏移 0/9/12/16 是相对 IP 头,帧内需 +14 Ethernet 头)
fn parse_ipv4(
  data : Bytes,
  ts_sec : Int64,
  ts_usec : Int,
  local_ips : FixedArray[IpAddr],
) -> ParsedPacket? {
  let total = data.length()
  let l3 = ETH_HDR
  guard total >= l3 + 20 else { return None }
  let vihl = data[l3].to_int()
  guard vihl >> 4 == 4 else { return None } // 非 IPv4
  let ihl = (vihl & 0x0F) * 4
  let ip_total = u16(data, l3 + 2)
  guard ip_total >= ihl && ip_total <= total - l3 else { return None }
  let proto = classify_proto(data[l3 + 9])
  let src = { v4: true, bytes: read_bytes(data, l3 + 12, 4) }
  let dst = { v4: true, bytes: read_bytes(data, l3 + 16, 4) }
  // 传输层解析,返回 (src_port, dst_port, payload_len)
  let l4 = match proto {
    Tcp => parse_tcp(data, l3 + ihl, ip_total - ihl)
    Udp => parse_udp(data, l3 + ihl, ip_total - ihl)
    _ => Some((0, 0, ip_total - ihl))
  }
  let (sport, dport, payload) = match l4 {
    Some(x) => x
    None => return None
  }
  Some(
    ParsedPacket::new(
      ts_sec,
      ts_usec,
      { src_ip: src, dst_ip: dst, src_port: sport, dst_port: dport, proto },
      direction(src, dst, local_ips),
      payload,
      ip_total,
    ),
  )
}