Skip to content

Commit 5d48440

Browse files
authored
Merge pull request #22 from doublegate/copilot/sub-pr-18
fix: restore correct ScanResultRkyv with manual IpAddr serialization
2 parents 30d1655 + d2f4d6e commit 5d48440

File tree

3 files changed

+119
-162
lines changed

3 files changed

+119
-162
lines changed

crates/prtip-core/src/types.rs

Lines changed: 113 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,20 @@ impl Iterator for PortRangeIterator {
316316
}
317317

318318
/// State of a scanned port
319-
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Archive, RkyvSerialize, RkyvDeserialize)]
319+
#[derive(
320+
Debug,
321+
Clone,
322+
Copy,
323+
PartialEq,
324+
Eq,
325+
PartialOrd,
326+
Ord,
327+
Serialize,
328+
Deserialize,
329+
Archive,
330+
RkyvSerialize,
331+
RkyvDeserialize,
332+
)]
320333
#[rkyv(derive(Debug))]
321334
pub enum PortState {
322335
/// Port is open and accepting connections
@@ -477,29 +490,85 @@ impl fmt::Display for TimingTemplate {
477490
}
478491
}
479492

480-
/// Serializable representation of ScanResult for rkyv
481-
#[derive(Debug, Clone, Archive, RkyvSerialize, RkyvDeserialize)]
493+
/// rkyv-compatible serialization format for ScanResult
494+
///
495+
/// This type is optimized for zero-copy deserialization using rkyv.
496+
/// It stores all data in a format that can be directly interpreted from
497+
/// memory-mapped files without allocation.
498+
///
499+
/// # Manual Serialization for IpAddr
500+
///
501+
/// std::net::IpAddr does not implement rkyv traits, so we manually serialize
502+
/// to bytes and convert between IpAddr and byte arrays in the From implementations.
503+
///
504+
/// # Alignment Requirements
505+
///
506+
/// This structure must maintain proper alignment for rkyv's zero-copy
507+
/// deserialization. The fixed-size entry buffer (512 bytes) provides
508+
/// adequate alignment for typical rkyv requirements.
509+
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
482510
#[rkyv(derive(Debug))]
483511
pub struct ScanResultRkyv {
484-
target_ip: IpAddr,
485-
port: u16,
486-
state: PortState,
487-
response_time_nanos: u128,
488-
timestamp_nanos: i64,
489-
banner: Option<String>,
490-
service: Option<String>,
491-
version: Option<String>,
492-
raw_response: Option<Vec<u8>>,
512+
/// Target IP address (16 bytes for IPv6 compatibility)
513+
pub target_ip_bytes: [u8; 16],
514+
/// Whether the IP is IPv4 (true) or IPv6 (false)
515+
pub is_ipv4: bool,
516+
/// Port number
517+
pub port: u16,
518+
/// Port state as u8 (Open=0, Closed=1, Filtered=2, Unknown=3)
519+
pub state: u8,
520+
/// Response time in nanoseconds (u64 to avoid truncation)
521+
pub response_time_nanos: u64,
522+
/// Timestamp in nanoseconds since Unix epoch
523+
pub timestamp_nanos: i64,
524+
/// Optional banner (max 128 bytes)
525+
pub banner: Option<String>,
526+
/// Optional service name (max 32 bytes)
527+
pub service: Option<String>,
528+
/// Optional service version (max 64 bytes)
529+
pub version: Option<String>,
530+
/// Optional raw response (limited to 256 bytes to fit in entry)
531+
pub raw_response: Option<Vec<u8>>,
493532
}
494533

495534
impl From<&ScanResult> for ScanResultRkyv {
496535
fn from(result: &ScanResult) -> Self {
536+
// Convert IpAddr to bytes
537+
let (target_ip_bytes, is_ipv4) = match result.target_ip {
538+
IpAddr::V4(ipv4) => {
539+
let mut bytes = [0u8; 16];
540+
bytes[..4].copy_from_slice(&ipv4.octets());
541+
(bytes, true)
542+
}
543+
IpAddr::V6(ipv6) => (ipv6.octets(), false),
544+
};
545+
546+
// Convert PortState to u8
547+
let state = match result.state {
548+
PortState::Open => 0,
549+
PortState::Closed => 1,
550+
PortState::Filtered => 2,
551+
PortState::Unknown => 3,
552+
};
553+
554+
// Convert response time to u64 nanoseconds (avoid truncation issues)
555+
// Note: u64 can represent up to ~584 years, which is more than sufficient
556+
// for network response times. We clamp to u64::MAX to avoid overflow.
557+
let response_time_nanos = result.response_time.as_nanos().min(u64::MAX as u128) as u64;
558+
559+
// Convert timestamp with proper error handling
560+
let timestamp_nanos = result
561+
.timestamp
562+
.timestamp_nanos_opt()
563+
.expect("timestamp out of range for nanosecond representation");
564+
497565
Self {
498-
target_ip: result.target_ip,
566+
target_ip_bytes,
567+
is_ipv4,
499568
port: result.port,
500-
state: result.state,
501-
response_time_nanos: result.response_time.as_nanos(),
502-
timestamp_nanos: result.timestamp.timestamp_nanos_opt().unwrap_or(0),
569+
state,
570+
response_time_nanos,
571+
timestamp_nanos,
503572
banner: result.banner.clone(),
504573
service: result.service.clone(),
505574
version: result.version.clone(),
@@ -510,12 +579,36 @@ impl From<&ScanResult> for ScanResultRkyv {
510579

511580
impl From<ScanResultRkyv> for ScanResult {
512581
fn from(rkyv: ScanResultRkyv) -> Self {
582+
// Convert bytes back to IpAddr
583+
let target_ip = if rkyv.is_ipv4 {
584+
let mut octets = [0u8; 4];
585+
octets.copy_from_slice(&rkyv.target_ip_bytes[..4]);
586+
IpAddr::V4(std::net::Ipv4Addr::from(octets))
587+
} else {
588+
IpAddr::V6(std::net::Ipv6Addr::from(rkyv.target_ip_bytes))
589+
};
590+
591+
// Convert u8 back to PortState
592+
let state = match rkyv.state {
593+
0 => PortState::Open,
594+
1 => PortState::Closed,
595+
2 => PortState::Filtered,
596+
_ => PortState::Unknown,
597+
};
598+
599+
// Convert u64 nanoseconds back to Duration
600+
// Safe: u64::MAX nanoseconds fits within Duration's range
601+
let response_time = Duration::from_nanos(rkyv.response_time_nanos);
602+
603+
// Convert i64 nanoseconds back to DateTime
604+
let timestamp = DateTime::from_timestamp_nanos(rkyv.timestamp_nanos);
605+
513606
Self {
514-
target_ip: rkyv.target_ip,
607+
target_ip,
515608
port: rkyv.port,
516-
state: rkyv.state,
517-
response_time: Duration::from_nanos(rkyv.response_time_nanos as u64),
518-
timestamp: DateTime::from_timestamp_nanos(rkyv.timestamp_nanos),
609+
state,
610+
response_time,
611+
timestamp,
519612
banner: rkyv.banner,
520613
service: rkyv.service,
521614
version: rkyv.version,
@@ -641,128 +734,6 @@ impl fmt::Display for ScanResult {
641734
}
642735
}
643736

644-
/// rkyv-compatible serialization format for ScanResult
645-
///
646-
/// This type is optimized for zero-copy deserialization using rkyv.
647-
/// It stores all data in a format that can be directly interpreted from
648-
/// memory-mapped files without allocation.
649-
///
650-
/// # Alignment Requirements
651-
///
652-
/// This structure must maintain proper alignment for rkyv's zero-copy
653-
/// deserialization. The fixed-size entry buffer (512 bytes) provides
654-
/// adequate alignment for typical rkyv requirements.
655-
#[derive(Debug, Clone, rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)]
656-
#[rkyv(derive(Debug))]
657-
pub struct ScanResultRkyv {
658-
/// Target IP address (16 bytes for IPv6 compatibility)
659-
pub target_ip_bytes: [u8; 16],
660-
/// Whether the IP is IPv4 (true) or IPv6 (false)
661-
pub is_ipv4: bool,
662-
/// Port number
663-
pub port: u16,
664-
/// Port state as u8 (Open=0, Closed=1, Filtered=2, Unknown=3)
665-
pub state: u8,
666-
/// Response time in nanoseconds (u64 to avoid truncation)
667-
pub response_time_nanos: u64,
668-
/// Timestamp in nanoseconds since Unix epoch
669-
pub timestamp_nanos: i64,
670-
/// Optional banner (max 128 bytes)
671-
pub banner: Option<String>,
672-
/// Optional service name (max 32 bytes)
673-
pub service: Option<String>,
674-
/// Optional service version (max 64 bytes)
675-
pub version: Option<String>,
676-
/// Optional raw response (limited to 256 bytes to fit in entry)
677-
pub raw_response: Option<Vec<u8>>,
678-
}
679-
680-
impl From<&ScanResult> for ScanResultRkyv {
681-
fn from(result: &ScanResult) -> Self {
682-
// Convert IpAddr to bytes
683-
let (target_ip_bytes, is_ipv4) = match result.target_ip {
684-
IpAddr::V4(ipv4) => {
685-
let mut bytes = [0u8; 16];
686-
bytes[..4].copy_from_slice(&ipv4.octets());
687-
(bytes, true)
688-
}
689-
IpAddr::V6(ipv6) => (ipv6.octets(), false),
690-
};
691-
692-
// Convert PortState to u8
693-
let state = match result.state {
694-
PortState::Open => 0,
695-
PortState::Closed => 1,
696-
PortState::Filtered => 2,
697-
PortState::Unknown => 3,
698-
};
699-
700-
// Convert response time to u64 nanoseconds (avoid truncation issues)
701-
// Note: u64 can represent up to ~584 years, which is more than sufficient
702-
// for network response times. We clamp to u64::MAX to avoid overflow.
703-
let response_time_nanos = result.response_time.as_nanos().min(u64::MAX as u128) as u64;
704-
705-
// Convert timestamp with proper error handling
706-
let timestamp_nanos = result
707-
.timestamp
708-
.timestamp_nanos_opt()
709-
.expect("timestamp out of range for nanosecond representation");
710-
711-
Self {
712-
target_ip_bytes,
713-
is_ipv4,
714-
port: result.port,
715-
state,
716-
response_time_nanos,
717-
timestamp_nanos,
718-
banner: result.banner.clone(),
719-
service: result.service.clone(),
720-
version: result.version.clone(),
721-
raw_response: result.raw_response.clone(),
722-
}
723-
}
724-
}
725-
726-
impl From<ScanResultRkyv> for ScanResult {
727-
fn from(rkyv: ScanResultRkyv) -> Self {
728-
// Convert bytes back to IpAddr
729-
let target_ip = if rkyv.is_ipv4 {
730-
let mut octets = [0u8; 4];
731-
octets.copy_from_slice(&rkyv.target_ip_bytes[..4]);
732-
IpAddr::V4(std::net::Ipv4Addr::from(octets))
733-
} else {
734-
IpAddr::V6(std::net::Ipv6Addr::from(rkyv.target_ip_bytes))
735-
};
736-
737-
// Convert u8 back to PortState
738-
let state = match rkyv.state {
739-
0 => PortState::Open,
740-
1 => PortState::Closed,
741-
2 => PortState::Filtered,
742-
_ => PortState::Unknown,
743-
};
744-
745-
// Convert u64 nanoseconds back to Duration
746-
// Safe: u64::MAX nanoseconds fits within Duration's range
747-
let response_time = Duration::from_nanos(rkyv.response_time_nanos);
748-
749-
// Convert i64 nanoseconds back to DateTime
750-
let timestamp = DateTime::from_timestamp_nanos(rkyv.timestamp_nanos);
751-
752-
Self {
753-
target_ip,
754-
port: rkyv.port,
755-
state,
756-
response_time,
757-
timestamp,
758-
banner: rkyv.banner,
759-
service: rkyv.service,
760-
version: rkyv.version,
761-
raw_response: rkyv.raw_response,
762-
}
763-
}
764-
}
765-
766737
/// Port filtering for exclusion/inclusion lists
767738
///
768739
/// Provides efficient port filtering using hash sets for O(1) lookups.

crates/prtip-scanner/src/output/mmap_reader.rs

Lines changed: 5 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -87,33 +87,19 @@ impl MmapResultReader {
8787
}
8888

8989
let offset = HEADER_SIZE + (index * self.entry_size);
90-
90+
9191
// Read length prefix (8 bytes)
92-
let len_bytes: [u8; 8] = self.mmap[offset..offset + LENGTH_PREFIX_SIZE].try_into().ok()?;
92+
let len_bytes: [u8; 8] = self.mmap[offset..offset + LENGTH_PREFIX_SIZE]
93+
.try_into()
94+
.ok()?;
9395
let len = u64::from_le_bytes(len_bytes) as usize;
94-
95-
if len == 0 || len + LENGTH_PREFIX_SIZE > self.entry_size {
96-
return None;
97-
}
98-
99-
// Read length prefix (u64 in little-endian)
100-
let len = u64::from_le_bytes(
101-
entry_bytes[..LENGTH_PREFIX_SIZE]
102-
.try_into()
103-
.expect("LENGTH_PREFIX_SIZE is 8 bytes"),
104-
) as usize;
10596

106-
// Validate length
10797
if len == 0 || len + LENGTH_PREFIX_SIZE > self.entry_size {
108-
eprintln!(
109-
"MmapResultReader: invalid entry length {} at index {}",
110-
len, index
111-
);
11298
return None;
11399
}
114100

115101
// Use zero-copy deserialization without unnecessary allocation
116-
let data_bytes = &entry_bytes[LENGTH_PREFIX_SIZE..LENGTH_PREFIX_SIZE + len];
102+
let data_bytes = &self.mmap[offset + LENGTH_PREFIX_SIZE..offset + LENGTH_PREFIX_SIZE + len];
117103
match rkyv::from_bytes::<ScanResultRkyv, rkyv::rancor::Error>(data_bytes) {
118104
Ok(rkyv_result) => Some(ScanResult::from(rkyv_result)),
119105
Err(e) => {

docs/archive/19-PHASE4-ENHANCEMENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1584,7 +1584,7 @@ Create comprehensive usage examples library, common scenarios guide, update all
15841584
- Insights: Performance comparison, use case recommendations
15851585

15861586
2. ~~**"Top Network Scanners Compared: Nmap, Masscan, ZMap, and More"** (findsec.org)~~
1587-
- ~~URL: https://findsec.org/index.php/blog/493-nmap-vs-masscan-zmap-rustscan-comparison~~ (Link no longer accessible)
1587+
- URL: (Link no longer accessible - findsec.org/index.php/blog/493-nmap-vs-masscan-zmap-rustscan-comparison)
15881588
- Insights: Feature matrix, speed benchmarks, tool selection guide
15891589

15901590
3. **"01/31/2025 – masscan vs nmap Scan"** (victsao.wordpress.com)

0 commit comments

Comments
 (0)