diff --git a/crates/prtip-core/src/types.rs b/crates/prtip-core/src/types.rs index 3271e60..af26cb5 100644 --- a/crates/prtip-core/src/types.rs +++ b/crates/prtip-core/src/types.rs @@ -316,7 +316,20 @@ impl Iterator for PortRangeIterator { } /// State of a scanned port -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Archive, RkyvSerialize, RkyvDeserialize)] +#[derive( + Debug, + Clone, + Copy, + PartialEq, + Eq, + PartialOrd, + Ord, + Serialize, + Deserialize, + Archive, + RkyvSerialize, + RkyvDeserialize, +)] #[rkyv(derive(Debug))] pub enum PortState { /// Port is open and accepting connections @@ -477,29 +490,85 @@ impl fmt::Display for TimingTemplate { } } -/// Serializable representation of ScanResult for rkyv -#[derive(Debug, Clone, Archive, RkyvSerialize, RkyvDeserialize)] +/// rkyv-compatible serialization format for ScanResult +/// +/// This type is optimized for zero-copy deserialization using rkyv. +/// It stores all data in a format that can be directly interpreted from +/// memory-mapped files without allocation. +/// +/// # Manual Serialization for IpAddr +/// +/// std::net::IpAddr does not implement rkyv traits, so we manually serialize +/// to bytes and convert between IpAddr and byte arrays in the From implementations. +/// +/// # Alignment Requirements +/// +/// This structure must maintain proper alignment for rkyv's zero-copy +/// deserialization. The fixed-size entry buffer (512 bytes) provides +/// adequate alignment for typical rkyv requirements. +#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] #[rkyv(derive(Debug))] pub struct ScanResultRkyv { - target_ip: IpAddr, - port: u16, - state: PortState, - response_time_nanos: u128, - timestamp_nanos: i64, - banner: Option, - service: Option, - version: Option, - raw_response: Option>, + /// Target IP address (16 bytes for IPv6 compatibility) + pub target_ip_bytes: [u8; 16], + /// Whether the IP is IPv4 (true) or IPv6 (false) + pub is_ipv4: bool, + /// Port number + pub port: u16, + /// Port state as u8 (Open=0, Closed=1, Filtered=2, Unknown=3) + pub state: u8, + /// Response time in nanoseconds (u64 to avoid truncation) + pub response_time_nanos: u64, + /// Timestamp in nanoseconds since Unix epoch + pub timestamp_nanos: i64, + /// Optional banner (max 128 bytes) + pub banner: Option, + /// Optional service name (max 32 bytes) + pub service: Option, + /// Optional service version (max 64 bytes) + pub version: Option, + /// Optional raw response (limited to 256 bytes to fit in entry) + pub raw_response: Option>, } impl From<&ScanResult> for ScanResultRkyv { fn from(result: &ScanResult) -> Self { + // Convert IpAddr to bytes + let (target_ip_bytes, is_ipv4) = match result.target_ip { + IpAddr::V4(ipv4) => { + let mut bytes = [0u8; 16]; + bytes[..4].copy_from_slice(&ipv4.octets()); + (bytes, true) + } + IpAddr::V6(ipv6) => (ipv6.octets(), false), + }; + + // Convert PortState to u8 + let state = match result.state { + PortState::Open => 0, + PortState::Closed => 1, + PortState::Filtered => 2, + PortState::Unknown => 3, + }; + + // Convert response time to u64 nanoseconds (avoid truncation issues) + // Note: u64 can represent up to ~584 years, which is more than sufficient + // for network response times. We clamp to u64::MAX to avoid overflow. + let response_time_nanos = result.response_time.as_nanos().min(u64::MAX as u128) as u64; + + // Convert timestamp with proper error handling + let timestamp_nanos = result + .timestamp + .timestamp_nanos_opt() + .expect("timestamp out of range for nanosecond representation"); + Self { - target_ip: result.target_ip, + target_ip_bytes, + is_ipv4, port: result.port, - state: result.state, - response_time_nanos: result.response_time.as_nanos(), - timestamp_nanos: result.timestamp.timestamp_nanos_opt().unwrap_or(0), + state, + response_time_nanos, + timestamp_nanos, banner: result.banner.clone(), service: result.service.clone(), version: result.version.clone(), @@ -510,12 +579,36 @@ impl From<&ScanResult> for ScanResultRkyv { impl From for ScanResult { fn from(rkyv: ScanResultRkyv) -> Self { + // Convert bytes back to IpAddr + let target_ip = if rkyv.is_ipv4 { + let mut octets = [0u8; 4]; + octets.copy_from_slice(&rkyv.target_ip_bytes[..4]); + IpAddr::V4(std::net::Ipv4Addr::from(octets)) + } else { + IpAddr::V6(std::net::Ipv6Addr::from(rkyv.target_ip_bytes)) + }; + + // Convert u8 back to PortState + let state = match rkyv.state { + 0 => PortState::Open, + 1 => PortState::Closed, + 2 => PortState::Filtered, + _ => PortState::Unknown, + }; + + // Convert u64 nanoseconds back to Duration + // Safe: u64::MAX nanoseconds fits within Duration's range + let response_time = Duration::from_nanos(rkyv.response_time_nanos); + + // Convert i64 nanoseconds back to DateTime + let timestamp = DateTime::from_timestamp_nanos(rkyv.timestamp_nanos); + Self { - target_ip: rkyv.target_ip, + target_ip, port: rkyv.port, - state: rkyv.state, - response_time: Duration::from_nanos(rkyv.response_time_nanos as u64), - timestamp: DateTime::from_timestamp_nanos(rkyv.timestamp_nanos), + state, + response_time, + timestamp, banner: rkyv.banner, service: rkyv.service, version: rkyv.version, @@ -641,128 +734,6 @@ impl fmt::Display for ScanResult { } } -/// rkyv-compatible serialization format for ScanResult -/// -/// This type is optimized for zero-copy deserialization using rkyv. -/// It stores all data in a format that can be directly interpreted from -/// memory-mapped files without allocation. -/// -/// # Alignment Requirements -/// -/// This structure must maintain proper alignment for rkyv's zero-copy -/// deserialization. The fixed-size entry buffer (512 bytes) provides -/// adequate alignment for typical rkyv requirements. -#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)] -#[rkyv(derive(Debug))] -pub struct ScanResultRkyv { - /// Target IP address (16 bytes for IPv6 compatibility) - pub target_ip_bytes: [u8; 16], - /// Whether the IP is IPv4 (true) or IPv6 (false) - pub is_ipv4: bool, - /// Port number - pub port: u16, - /// Port state as u8 (Open=0, Closed=1, Filtered=2, Unknown=3) - pub state: u8, - /// Response time in nanoseconds (u64 to avoid truncation) - pub response_time_nanos: u64, - /// Timestamp in nanoseconds since Unix epoch - pub timestamp_nanos: i64, - /// Optional banner (max 128 bytes) - pub banner: Option, - /// Optional service name (max 32 bytes) - pub service: Option, - /// Optional service version (max 64 bytes) - pub version: Option, - /// Optional raw response (limited to 256 bytes to fit in entry) - pub raw_response: Option>, -} - -impl From<&ScanResult> for ScanResultRkyv { - fn from(result: &ScanResult) -> Self { - // Convert IpAddr to bytes - let (target_ip_bytes, is_ipv4) = match result.target_ip { - IpAddr::V4(ipv4) => { - let mut bytes = [0u8; 16]; - bytes[..4].copy_from_slice(&ipv4.octets()); - (bytes, true) - } - IpAddr::V6(ipv6) => (ipv6.octets(), false), - }; - - // Convert PortState to u8 - let state = match result.state { - PortState::Open => 0, - PortState::Closed => 1, - PortState::Filtered => 2, - PortState::Unknown => 3, - }; - - // Convert response time to u64 nanoseconds (avoid truncation issues) - // Note: u64 can represent up to ~584 years, which is more than sufficient - // for network response times. We clamp to u64::MAX to avoid overflow. - let response_time_nanos = result.response_time.as_nanos().min(u64::MAX as u128) as u64; - - // Convert timestamp with proper error handling - let timestamp_nanos = result - .timestamp - .timestamp_nanos_opt() - .expect("timestamp out of range for nanosecond representation"); - - Self { - target_ip_bytes, - is_ipv4, - port: result.port, - state, - response_time_nanos, - timestamp_nanos, - banner: result.banner.clone(), - service: result.service.clone(), - version: result.version.clone(), - raw_response: result.raw_response.clone(), - } - } -} - -impl From for ScanResult { - fn from(rkyv: ScanResultRkyv) -> Self { - // Convert bytes back to IpAddr - let target_ip = if rkyv.is_ipv4 { - let mut octets = [0u8; 4]; - octets.copy_from_slice(&rkyv.target_ip_bytes[..4]); - IpAddr::V4(std::net::Ipv4Addr::from(octets)) - } else { - IpAddr::V6(std::net::Ipv6Addr::from(rkyv.target_ip_bytes)) - }; - - // Convert u8 back to PortState - let state = match rkyv.state { - 0 => PortState::Open, - 1 => PortState::Closed, - 2 => PortState::Filtered, - _ => PortState::Unknown, - }; - - // Convert u64 nanoseconds back to Duration - // Safe: u64::MAX nanoseconds fits within Duration's range - let response_time = Duration::from_nanos(rkyv.response_time_nanos); - - // Convert i64 nanoseconds back to DateTime - let timestamp = DateTime::from_timestamp_nanos(rkyv.timestamp_nanos); - - Self { - target_ip, - port: rkyv.port, - state, - response_time, - timestamp, - banner: rkyv.banner, - service: rkyv.service, - version: rkyv.version, - raw_response: rkyv.raw_response, - } - } -} - /// Port filtering for exclusion/inclusion lists /// /// Provides efficient port filtering using hash sets for O(1) lookups. diff --git a/crates/prtip-scanner/src/output/mmap_reader.rs b/crates/prtip-scanner/src/output/mmap_reader.rs index 8b7e894..0e6713f 100644 --- a/crates/prtip-scanner/src/output/mmap_reader.rs +++ b/crates/prtip-scanner/src/output/mmap_reader.rs @@ -87,33 +87,19 @@ impl MmapResultReader { } let offset = HEADER_SIZE + (index * self.entry_size); - + // Read length prefix (8 bytes) - let len_bytes: [u8; 8] = self.mmap[offset..offset + LENGTH_PREFIX_SIZE].try_into().ok()?; + let len_bytes: [u8; 8] = self.mmap[offset..offset + LENGTH_PREFIX_SIZE] + .try_into() + .ok()?; let len = u64::from_le_bytes(len_bytes) as usize; - - if len == 0 || len + LENGTH_PREFIX_SIZE > self.entry_size { - return None; - } - - // Read length prefix (u64 in little-endian) - let len = u64::from_le_bytes( - entry_bytes[..LENGTH_PREFIX_SIZE] - .try_into() - .expect("LENGTH_PREFIX_SIZE is 8 bytes"), - ) as usize; - // Validate length if len == 0 || len + LENGTH_PREFIX_SIZE > self.entry_size { - eprintln!( - "MmapResultReader: invalid entry length {} at index {}", - len, index - ); return None; } // Use zero-copy deserialization without unnecessary allocation - let data_bytes = &entry_bytes[LENGTH_PREFIX_SIZE..LENGTH_PREFIX_SIZE + len]; + let data_bytes = &self.mmap[offset + LENGTH_PREFIX_SIZE..offset + LENGTH_PREFIX_SIZE + len]; match rkyv::from_bytes::(data_bytes) { Ok(rkyv_result) => Some(ScanResult::from(rkyv_result)), Err(e) => { diff --git a/docs/archive/19-PHASE4-ENHANCEMENTS.md b/docs/archive/19-PHASE4-ENHANCEMENTS.md index 2f617f4..0014007 100644 --- a/docs/archive/19-PHASE4-ENHANCEMENTS.md +++ b/docs/archive/19-PHASE4-ENHANCEMENTS.md @@ -1584,7 +1584,7 @@ Create comprehensive usage examples library, common scenarios guide, update all - Insights: Performance comparison, use case recommendations 2. ~~**"Top Network Scanners Compared: Nmap, Masscan, ZMap, and More"** (findsec.org)~~ - - ~~URL: https://findsec.org/index.php/blog/493-nmap-vs-masscan-zmap-rustscan-comparison~~ (Link no longer accessible) + - URL: (Link no longer accessible - findsec.org/index.php/blog/493-nmap-vs-masscan-zmap-rustscan-comparison) - Insights: Feature matrix, speed benchmarks, tool selection guide 3. **"01/31/2025 – masscan vs nmap Scan"** (victsao.wordpress.com)