use crate::api::{SubdomainResult, SubdomainScanData};
use crate::handle::{DiscoveredDomain, SummaryStats, VerificationResult};
use crate::input::OutputFormat;
use serde::{Deserialize, Serialize};
use std::fs::File;
use std::io::Write;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableDiscoveredDomain {
pub domain: String,
pub ip: String,
pub record_type: String,
pub timestamp: u64,
pub formatted_time: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableVerificationResult {
pub domain: String,
pub ip: String,
pub http_status: Option<u16>,
pub https_status: Option<u16>,
pub title: Option<String>,
pub server: Option<String>,
pub is_alive: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SerializableSummaryStats {
pub total_domains: usize,
pub unique_ips: Vec<String>,
pub ip_ranges: std::collections::HashMap<String, Vec<String>>,
pub record_types: std::collections::HashMap<String, usize>,
pub verified_domains: usize,
pub alive_domains: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ExportData {
pub discovered_domains: Vec<SerializableDiscoveredDomain>,
pub verification_results: Vec<SerializableVerificationResult>,
pub summary: SerializableSummaryStats,
pub export_time: String,
}
impl From<DiscoveredDomain> for SerializableDiscoveredDomain {
fn from(domain: DiscoveredDomain) -> Self {
let formatted_time = chrono::DateTime::from_timestamp(domain.timestamp as i64, 0)
.unwrap_or_default()
.format("%Y-%m-%d %H:%M:%S")
.to_string();
SerializableDiscoveredDomain {
domain: domain.domain,
ip: domain.ip,
record_type: domain.record_type,
timestamp: domain.timestamp,
formatted_time,
}
}
}
impl From<VerificationResult> for SerializableVerificationResult {
fn from(result: VerificationResult) -> Self {
SerializableVerificationResult {
domain: result.domain,
ip: result.ip,
http_status: result.http_status,
https_status: result.https_status,
title: result.title,
server: result.server,
is_alive: result.is_alive,
}
}
}
impl From<SummaryStats> for SerializableSummaryStats {
fn from(stats: SummaryStats) -> Self {
SerializableSummaryStats {
total_domains: stats.total_domains,
unique_ips: stats.unique_ips.into_iter().collect(),
ip_ranges: stats.ip_ranges,
record_types: stats.record_types,
verified_domains: stats.verified_domains,
alive_domains: stats.alive_domains,
}
}
}
pub fn export_results(
discovered: Vec<DiscoveredDomain>,
verified: Vec<VerificationResult>,
summary: SummaryStats,
output_path: &str,
format: &OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let export_data = ExportData {
discovered_domains: discovered.into_iter().map(|d| d.into()).collect(),
verification_results: verified.into_iter().map(|v| v.into()).collect(),
summary: summary.into(),
export_time: chrono::Utc::now()
.format("%Y-%m-%d %H:%M:%S UTC")
.to_string(),
};
let mut file = File::create(output_path)?;
match format {
OutputFormat::Json => {
let json_data = serde_json::to_string_pretty(&export_data)?;
file.write_all(json_data.as_bytes())?;
}
OutputFormat::Xml => {
let xml_data = export_to_xml(&export_data)?;
file.write_all(xml_data.as_bytes())?;
}
OutputFormat::Csv => {
let csv_data = export_to_csv(&export_data)?;
file.write_all(csv_data.as_bytes())?;
}
OutputFormat::Txt => {
let txt_data = export_to_txt(&export_data)?;
file.write_all(txt_data.as_bytes())?;
}
}
println!("结果已导出到: {}", output_path);
Ok(())
}
pub fn export_scan_data(
scan_data: &SubdomainScanData,
output_path: &str,
format: &OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
export_results(
scan_data.discovered_domains.clone(),
scan_data.verification_results.clone(),
scan_data.summary.clone(),
output_path,
format,
)
}
pub fn export_subdomain_results(
results: &[SubdomainResult],
output_path: &str,
format: &OutputFormat,
) -> Result<(), Box<dyn std::error::Error>> {
let scan_data = SubdomainScanData::from_results(results);
export_scan_data(&scan_data, output_path, format)
}
fn export_to_xml(data: &ExportData) -> Result<String, Box<dyn std::error::Error>> {
let mut xml = String::new();
xml.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
xml.push_str("<rsubdomain_results>\n");
xml.push_str(&format!(
" <export_time>{}</export_time>\n",
data.export_time
));
xml.push_str(" <summary>\n");
xml.push_str(&format!(
" <total_domains>{}</total_domains>\n",
data.summary.total_domains
));
xml.push_str(&format!(
" <unique_ips_count>{}</unique_ips_count>\n",
data.summary.unique_ips.len()
));
xml.push_str(&format!(
" <verified_domains>{}</verified_domains>\n",
data.summary.verified_domains
));
xml.push_str(&format!(
" <alive_domains>{}</alive_domains>\n",
data.summary.alive_domains
));
xml.push_str(" </summary>\n");
xml.push_str(" <discovered_domains>\n");
for domain in &data.discovered_domains {
xml.push_str(" <domain>\n");
xml.push_str(&format!(
" <name>{}</name>\n",
escape_xml(&domain.domain)
));
xml.push_str(&format!(" <ip>{}</ip>\n", escape_xml(&domain.ip)));
xml.push_str(&format!(
" <record_type>{}</record_type>\n",
escape_xml(&domain.record_type)
));
xml.push_str(&format!(
" <timestamp>{}</timestamp>\n",
domain.timestamp
));
xml.push_str(&format!(
" <formatted_time>{}</formatted_time>\n",
escape_xml(&domain.formatted_time)
));
xml.push_str(" </domain>\n");
}
xml.push_str(" </discovered_domains>\n");
xml.push_str(" <verification_results>\n");
for result in &data.verification_results {
xml.push_str(" <result>\n");
xml.push_str(&format!(
" <domain>{}</domain>\n",
escape_xml(&result.domain)
));
xml.push_str(&format!(" <ip>{}</ip>\n", escape_xml(&result.ip)));
xml.push_str(&format!(
" <http_status>{}</http_status>\n",
result
.http_status
.map_or("N/A".to_string(), |s| s.to_string())
));
xml.push_str(&format!(
" <https_status>{}</https_status>\n",
result
.https_status
.map_or("N/A".to_string(), |s| s.to_string())
));
xml.push_str(&format!(
" <title>{}</title>\n",
escape_xml(result.title.as_deref().unwrap_or("N/A"))
));
xml.push_str(&format!(
" <server>{}</server>\n",
escape_xml(result.server.as_deref().unwrap_or("N/A"))
));
xml.push_str(&format!(" <is_alive>{}</is_alive>\n", result.is_alive));
xml.push_str(" </result>\n");
}
xml.push_str(" </verification_results>\n");
xml.push_str("</rsubdomain_results>\n");
Ok(xml)
}
fn export_to_csv(data: &ExportData) -> Result<String, Box<dyn std::error::Error>> {
let mut csv = String::new();
csv.push_str("# 发现的域名\n");
csv.push_str("Domain,IP,RecordType,Timestamp,FormattedTime\n");
for domain in &data.discovered_domains {
csv.push_str(&format!(
"{},{},{},{},{}\n",
escape_csv(&domain.domain),
escape_csv(&domain.ip),
escape_csv(&domain.record_type),
domain.timestamp,
escape_csv(&domain.formatted_time)
));
}
csv.push_str("\n# 验证结果\n");
csv.push_str("Domain,IP,HTTPStatus,HTTPSStatus,Title,Server,IsAlive\n");
for result in &data.verification_results {
csv.push_str(&format!(
"{},{},{},{},{},{},{}\n",
escape_csv(&result.domain),
escape_csv(&result.ip),
result
.http_status
.map_or("N/A".to_string(), |s| s.to_string()),
result
.https_status
.map_or("N/A".to_string(), |s| s.to_string()),
escape_csv(result.title.as_deref().unwrap_or("N/A")),
escape_csv(result.server.as_deref().unwrap_or("N/A")),
result.is_alive
));
}
Ok(csv)
}
fn export_to_txt(data: &ExportData) -> Result<String, Box<dyn std::error::Error>> {
let mut txt = String::new();
txt.push_str(&format!("rsubdomain 扫描结果报告\n"));
txt.push_str(&format!("导出时间: {}\n", data.export_time));
txt.push_str(&format!("{}\n\n", "=".repeat(60)));
txt.push_str("汇总统计:\n");
txt.push_str(&format!(" 发现域名总数: {}\n", data.summary.total_domains));
txt.push_str(&format!(
" 唯一IP数量: {}\n",
data.summary.unique_ips.len()
));
txt.push_str(&format!(
" 已验证域名: {}\n",
data.summary.verified_domains
));
txt.push_str(&format!(" 存活域名: {}\n", data.summary.alive_domains));
txt.push_str("\n");
txt.push_str("记录类型分布:\n");
for (record_type, count) in &data.summary.record_types {
txt.push_str(&format!(" {}: {}\n", record_type, count));
}
txt.push_str("\n");
txt.push_str("发现的域名:\n");
txt.push_str(&format!(
"{:<30} {:<15} {:<10} {:<20}\n",
"域名", "IP地址", "记录类型", "时间"
));
txt.push_str(&format!("{}\n", "-".repeat(80)));
for domain in &data.discovered_domains {
txt.push_str(&format!(
"{:<30} {:<15} {:<10} {:<20}\n",
domain.domain, domain.ip, domain.record_type, domain.formatted_time
));
}
txt.push_str("\n");
if !data.verification_results.is_empty() {
txt.push_str("验证结果:\n");
txt.push_str(&format!(
"{:<30} {:<15} {:<6} {:<6} {:<20} {:<10}\n",
"域名", "IP地址", "HTTP", "HTTPS", "标题", "存活"
));
txt.push_str(&format!("{}\n", "-".repeat(90)));
for result in &data.verification_results {
txt.push_str(&format!(
"{:<30} {:<15} {:<6} {:<6} {:<20} {:<10}\n",
result.domain,
result.ip,
result
.http_status
.map_or("N/A".to_string(), |s| s.to_string()),
result
.https_status
.map_or("N/A".to_string(), |s| s.to_string()),
result.title.as_deref().unwrap_or("N/A"),
if result.is_alive { "YES" } else { "NO" }
));
}
}
Ok(txt)
}
fn escape_xml(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
fn escape_csv(s: &str) -> String {
if s.contains(',') || s.contains('"') || s.contains('\n') {
format!("\"{}\"", s.replace('"', "\"\""))
} else {
s.to_string()
}
}