├── .github └── workflows │ ├── release.yml │ └── rust.yml ├── Cargo.toml ├── LICENSE ├── README.md ├── config.json └── src ├── cli ├── args.rs └── mod.rs ├── config ├── mod.rs └── platform.rs ├── converter ├── fields.rs ├── mod.rs ├── operators.rs ├── query.rs └── validator.rs ├── error ├── mod.rs └── types.rs ├── lib.rs ├── main.rs └── output ├── formatter.rs └── mod.rs /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*.*.*' 7 | 8 | permissions: 9 | contents: write 10 | 11 | jobs: 12 | create-release: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: taiki-e/create-gh-release-action@v1 17 | with: 18 | # (required) GitHub token for creating GitHub Releases. 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | upload-assets: 22 | needs: create-release 23 | strategy: 24 | matrix: 25 | include: 26 | - target: aarch64-unknown-linux-gnu 27 | os: ubuntu-22.04 28 | - target: aarch64-apple-darwin 29 | os: macos-latest 30 | - target: x86_64-unknown-linux-gnu 31 | os: ubuntu-22.04 32 | - target: x86_64-apple-darwin 33 | os: macos-latest 34 | - target: universal-apple-darwin 35 | os: macos-latest 36 | - target: x86_64-pc-windows-msvc 37 | os: windows-latest 38 | runs-on: ${{ matrix.os }} 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: taiki-e/upload-rust-binary-action@v1 42 | with: 43 | # (required) Comma-separated list of binary names (non-extension portion of filename) to build and upload. 44 | # Note that glob pattern is not supported yet. 45 | bin: convertix 46 | # (optional) Target triple, default is host triple. 47 | target: ${{ matrix.target }} 48 | # (required) GitHub token for uploading assets to GitHub Releases. 49 | token: ${{ secrets.GITHUB_TOKEN }} 50 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose 23 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "convertix" 3 | version = "0.1.0" 4 | edition = "2021" 5 | description = "Cyberspace Asset Mapping Platform Query Statement Conversion Tool" 6 | license = "MIT" 7 | authors = ["EvilChen <24655118+gh0stkey@users.noreply.github.com>"] 8 | repository = "https://github.com/HACK-THE-WORLD/ConvertiX" 9 | keywords = ["cybersecurity", "query", "conversion", "fofa", "quake"] 10 | categories = ["command-line-utilities", "development-tools"] 11 | 12 | [[bin]] 13 | name = "convertix" 14 | path = "src/main.rs" 15 | 16 | [dependencies] 17 | serde = { version = "1.0", features = ["derive"] } 18 | serde_json = "1.0" 19 | regex = "1.0" 20 | clap = { version = "4.0", features = ["derive"] } 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 HACK THE WORLD 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ConvertiX 2 | 3 | 作者:key 4 | 5 | ## 项目介绍 6 | 7 | `ConvertiX` 是一款基于 `Rust 语言` 编写的网络安全工具,它主要用于常用网络空间测绘平台语句之间互相转换。支持五大测绘平台:FOFA、QUAKE、HUNTER、ZOOMEYE、THREATBOOK。输入任意一个平台的搜索语句即可获得其余四个平台转换后的语句。 8 | 9 | ## 项目使用 10 | 11 | 常用命令如下,将你输入的语句和平台进行对应的填入即可转换。 12 | 13 | ```shell 14 | # 最简单的方式 15 | ./ConvertiX -p fofa -q 搜索语句 16 | 17 | # 从文件读取搜索语句转换 18 | ./ConvertiX -p fofa -q @file.txt 19 | 20 | # 输出不同的格式 21 | # 默认格式 22 | ./ConvertiX -p fofa -q @file.txt -f raw 23 | # JSON格式 24 | ./ConvertiX -p fofa -q @file.txt -f json 25 | 26 | # 保存到文件 27 | ./ConvertiX -p fofa -q @file.txt -f raw -o result.txt 28 | ``` 29 | 30 | 通过`-h/--help`可以查看更详细的信息: 31 | 32 | ```shell 33 | [Cyberspace Asset Mapping Platform Query Statement Conversion Tool] 34 | 35 | Usage: ConvertiX [OPTIONS] --query --platform 36 | 37 | Options: 38 | -c, --config Configuration file path [default: config.json] 39 | -q, --query Query statement (use @filename to read from file) 40 | -p, --platform Source platform of the query statement 41 | -f, --format Output format [default: raw] [possible values: raw, json] 42 | -o, --output Output file path (optional, defaults to stdout) 43 | -h, --help Print help (see more with '--help') 44 | -V, --version Print version 45 | ``` 46 | 47 | ## 配置文件 48 | 49 | 项目包含一个标准的JSON配置文件 `config.json`,你可以根据需要修改或扩展,注意的是`operators`属于逻辑操作符,不允许增删改否则会出错。如果你想要支持更多平台,默认情况下也可以支持,在 `config.json` 中添加新平台的配置,在对应平台的 `fields` 配置中添加新的字段映射即可(字段映射要求每个平台都应有配置)。 50 | 51 | ```json 52 | { 53 | "fofa": { 54 | "fields": { 55 | "ip": "ip", 56 | "port": "port", 57 | "body": "body" 58 | }, 59 | "operators": { 60 | "equal": "=", 61 | "and": "&&", 62 | "or": "||", 63 | "not_equal": "!=", 64 | "left_paren": "(", 65 | "right_paren": ")" 66 | } 67 | } 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | { 2 | "fofa": { 3 | "fields": { 4 | "ip": "ip", 5 | "port": "port", 6 | "domain": "domain", 7 | "host": "host", 8 | "os": "os", 9 | "server": "server", 10 | "asn": "asn", 11 | "protocol": "protocol", 12 | "banner ": "banner", 13 | "title": "title", 14 | "header": "header", 15 | "body": "body", 16 | "icp": "icp", 17 | "country": "country", 18 | "region": "region", 19 | "city": "city", 20 | "cert": "cert", 21 | "cert.sn": "cert.sn" 22 | }, 23 | "operators": { 24 | "equal": "=", 25 | "and": "&&", 26 | "or": "||", 27 | "not_equal": "!=", 28 | "left_paren": "(", 29 | "right_paren": ")" 30 | } 31 | }, 32 | "quake": { 33 | "fields": { 34 | "ip": "ip", 35 | "port": "port", 36 | "domain": "domain", 37 | "host": "host", 38 | "os": "os", 39 | "server": "server", 40 | "asn": "asn", 41 | "protocol": "service", 42 | "banner ": "response", 43 | "title": "title", 44 | "header": "headers", 45 | "body": "body", 46 | "icp": "icp", 47 | "country": "country", 48 | "region": "province", 49 | "city": "city", 50 | "cert": "cert", 51 | "cert.sn": "tls_SN" 52 | }, 53 | "operators": { 54 | "equal": ":", 55 | "and": "AND", 56 | "or": "OR ", 57 | "not_equal": "NOT", 58 | "left_paren": "(", 59 | "right_paren": ")" 60 | } 61 | }, 62 | "zoomeye": { 63 | "fields": { 64 | "ip": "ip", 65 | "port": "port", 66 | "domain": "domain", 67 | "host": "hostname", 68 | "os": "os", 69 | "server": "http.header.server", 70 | "asn": "asn", 71 | "protocol": "protocol", 72 | "banner ": "response", 73 | "title": "title", 74 | "header": "http.header", 75 | "body": "http.body", 76 | "icp": "icp.number", 77 | "country": "country", 78 | "region": "subdivisions", 79 | "city": "city", 80 | "cert": "ssl", 81 | "cert.sn": "ssl.cert.serial" 82 | }, 83 | "operators": { 84 | "equal": "=", 85 | "and": "&&", 86 | "or": "||", 87 | "not_equal": "!=", 88 | "left_paren": "(", 89 | "right_paren": ")" 90 | } 91 | }, 92 | "hunter": { 93 | "fields": { 94 | "ip": "ip", 95 | "port": "ip.port", 96 | "domain": "domain", 97 | "host": "domain", 98 | "os": "ip.os", 99 | "server": "header.server", 100 | "asn": "as.number", 101 | "protocol": "protocol", 102 | "banner ": "protocol.banner", 103 | "title": "web.title", 104 | "header": "header", 105 | "body": "web.body", 106 | "icp": "icp.number", 107 | "country": "country", 108 | "region": "province", 109 | "city": "city", 110 | "cert": "cert", 111 | "cert.sn": "cert.serial_number" 112 | }, 113 | "operators": { 114 | "equal": "=", 115 | "and": "&&", 116 | "or": "||", 117 | "not_equal": "!=", 118 | "left_paren": "(", 119 | "right_paren": ")" 120 | } 121 | }, 122 | "threatbook": { 123 | "fields": { 124 | "ip": "ip", 125 | "port": "port", 126 | "domain": "domain", 127 | "host": "host", 128 | "os": "os", 129 | "server": "server", 130 | "asn": "asn", 131 | "protocol": "protocol", 132 | "banner ": "banner", 133 | "title": "title", 134 | "header": "header", 135 | "body": "body", 136 | "icp": "icp", 137 | "country": "country", 138 | "region": "region", 139 | "city": "city", 140 | "cert": "cert", 141 | "cert.sn": "cert.sn" 142 | }, 143 | "operators": { 144 | "equal": "=", 145 | "and": "&&", 146 | "or": "||", 147 | "not_equal": "!=", 148 | "left_paren": "(", 149 | "right_paren": ")" 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/cli/args.rs: -------------------------------------------------------------------------------- 1 | use clap::{Parser, ValueEnum}; 2 | use std::fs; 3 | use std::path::PathBuf; 4 | use std::process; 5 | 6 | /// Command line arguments 7 | #[derive(Parser)] 8 | #[command(name = "ConvertiX")] 9 | #[command(author = "key")] 10 | #[command(about = "[Cyberspace Asset Mapping Platform Query Statement Conversion Tool]")] 11 | #[command(version = "0.1.0")] 12 | pub struct Args { 13 | /// Configuration file path 14 | #[arg(short = 'c', long = "config", default_value = "config.json")] 15 | pub config: PathBuf, 16 | 17 | /// Query statement (use @filename to read from file) 18 | #[arg(short = 'q', long = "query")] 19 | pub query: String, 20 | 21 | /// Source platform of the query statement 22 | #[arg(short = 'p', long = "platform")] 23 | pub platform: String, 24 | 25 | /// Output format 26 | #[arg(short = 'f', long = "format", default_value = "raw")] 27 | pub format: OutputFormat, 28 | 29 | /// Output file path (optional, defaults to stdout) 30 | #[arg(short = 'o', long = "output")] 31 | pub output: Option, 32 | } 33 | 34 | /// Output format options 35 | #[derive(Clone, ValueEnum)] 36 | pub enum OutputFormat { 37 | /// Raw text output 38 | Raw, 39 | /// JSON format output 40 | Json, 41 | } 42 | 43 | impl Args { 44 | /// Parse query input, supporting file input with @ prefix 45 | pub fn parse_query_input(&self) -> String { 46 | if self.query.starts_with('@') { 47 | // Read from file 48 | let file_path = &self.query[1..]; // Remove @ prefix 49 | match fs::read_to_string(file_path) { 50 | Ok(content) => content.trim().to_string(), 51 | Err(e) => { 52 | eprintln!("ERROR: Failed to read query file '{}': {}", file_path, e); 53 | process::exit(1); 54 | } 55 | } 56 | } else { 57 | // Use input directly 58 | self.query.clone() 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod args; 2 | 3 | pub use args::*; 4 | -------------------------------------------------------------------------------- /src/config/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod platform; 2 | 3 | pub use platform::*; 4 | -------------------------------------------------------------------------------- /src/config/platform.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use std::collections::HashMap; 3 | use std::fs; 4 | use std::path::Path; 5 | use crate::error::{ConversionError, ConversionResult}; 6 | 7 | /// Platform operators configuration 8 | #[derive(Debug, Clone, Serialize, Deserialize)] 9 | pub struct Operators { 10 | pub equal: String, 11 | pub and: String, 12 | pub or: String, 13 | pub not_equal: String, 14 | pub left_paren: String, 15 | pub right_paren: String, 16 | } 17 | 18 | /// Platform configuration containing operators and field mappings 19 | #[derive(Debug, Clone, Serialize, Deserialize)] 20 | pub struct PlatformConfig { 21 | pub operators: Operators, 22 | pub fields: HashMap, 23 | } 24 | 25 | /// Configuration manager for all platforms 26 | #[derive(Debug, Clone)] 27 | pub struct ConfigManager { 28 | configs: HashMap, 29 | } 30 | 31 | impl ConfigManager { 32 | /// Load configuration from JSON file 33 | pub fn from_file>(config_path: P) -> ConversionResult { 34 | let config_content = fs::read_to_string(config_path) 35 | .map_err(|e| ConversionError::ConfigurationError(format!("Failed to read config file: {}", e)))?; 36 | 37 | let configs: HashMap = serde_json::from_str(&config_content) 38 | .map_err(|e| ConversionError::ConfigurationError(format!("Failed to parse config file: {}", e)))?; 39 | 40 | Ok(Self { configs }) 41 | } 42 | 43 | /// Get configuration for a specific platform 44 | pub fn get_platform_config(&self, platform: &str) -> ConversionResult<&PlatformConfig> { 45 | self.configs.get(platform) 46 | .ok_or_else(|| ConversionError::UnsupportedPlatform(platform.to_string())) 47 | } 48 | 49 | /// Get list of supported platforms 50 | pub fn get_supported_platforms(&self) -> Vec { 51 | self.configs.keys().cloned().collect() 52 | } 53 | 54 | /// Check if a platform is supported 55 | pub fn is_platform_supported(&self, platform: &str) -> bool { 56 | self.configs.contains_key(platform) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/converter/fields.rs: -------------------------------------------------------------------------------- 1 | use crate::config::PlatformConfig; 2 | use regex::Regex; 3 | 4 | /// Field converter for transforming field names between platforms 5 | pub struct FieldConverter; 6 | 7 | impl FieldConverter { 8 | /// Convert field prefixes between platforms 9 | pub fn convert_fields( 10 | query: &str, 11 | from_config: &PlatformConfig, 12 | to_config: &PlatformConfig, 13 | ) -> String { 14 | let mut result = query.to_string(); 15 | 16 | // Sort field pairs by field name length, process longer names first to avoid partial matching 17 | let mut field_pairs: Vec<_> = from_config.fields.iter().collect(); 18 | field_pairs.sort_by(|a, b| b.0.len().cmp(&a.0.len())); 19 | 20 | for (field_name, from_field_prefix) in field_pairs { 21 | if let Some(to_field_prefix) = to_config.fields.get(field_name) { 22 | if from_field_prefix != to_field_prefix { 23 | // Create patterns for both = and : operators 24 | let from_patterns = [ 25 | format!("{}=", from_field_prefix), 26 | format!("{}:", from_field_prefix), 27 | ]; 28 | 29 | for from_pattern in from_patterns { 30 | // Use regex for precise matching with word boundaries 31 | let from_pattern_escaped = regex::escape(&from_pattern); 32 | let pattern = format!(r"\b{}", from_pattern_escaped); 33 | 34 | if let Ok(re) = Regex::new(&pattern) { 35 | // Determine the target operator based on the original operator 36 | let replacement = if from_pattern.ends_with('=') { 37 | format!("{}=", to_field_prefix) 38 | } else { 39 | format!("{}:", to_field_prefix) 40 | }; 41 | result = re.replace_all(&result, replacement.as_str()).to_string(); 42 | } 43 | } 44 | } 45 | } 46 | } 47 | 48 | result 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/converter/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod query; 2 | pub mod operators; 3 | pub mod fields; 4 | pub mod validator; 5 | 6 | pub use query::*; 7 | pub use operators::*; 8 | pub use fields::*; 9 | pub use validator::*; 10 | -------------------------------------------------------------------------------- /src/converter/operators.rs: -------------------------------------------------------------------------------- 1 | use crate::config::PlatformConfig; 2 | use regex::Regex; 3 | 4 | /// Operator converter for transforming operators between platforms 5 | pub struct OperatorConverter; 6 | 7 | impl OperatorConverter { 8 | /// Convert not equal operators 9 | pub fn convert_not_equal_operator( 10 | query: &str, 11 | from_config: &PlatformConfig, 12 | to_config: &PlatformConfig, 13 | ) -> String { 14 | let mut result = query.to_string(); 15 | 16 | // If source and target not equal operators are the same, return directly 17 | if from_config.operators.not_equal == to_config.operators.not_equal { 18 | return result; 19 | } 20 | 21 | // Handle conversion from != format 22 | if from_config.operators.not_equal == "!=" { 23 | // Match field!="value" format 24 | let re = Regex::new(r#"(\w+)!="([^"]*)""#).unwrap(); 25 | result = re 26 | .replace_all(&result, |caps: ®ex::Captures| { 27 | let field = &caps[1]; 28 | let value = &caps[2]; 29 | 30 | // Convert based on target platform's not equal operator format 31 | if to_config.operators.not_equal.trim() == "NOT" { 32 | // Convert to NOT field:"value" format 33 | format!("{} {}:\"{}\"", to_config.operators.not_equal, field, value) 34 | } else { 35 | // Other formats, directly replace operator 36 | format!("{}{}\"{}\"", field, to_config.operators.not_equal, value) 37 | } 38 | }) 39 | .to_string(); 40 | } 41 | // Handle conversion from NOT format 42 | else if from_config.operators.not_equal.trim() == "NOT" { 43 | // Handle NOT field="value" format (FOFA style) 44 | let not_pattern = regex::escape(&from_config.operators.not_equal); 45 | let re_pattern = format!(r#"{}\s+(\w+)="([^"]*)""#, not_pattern); 46 | let re = Regex::new(&re_pattern).unwrap(); 47 | 48 | result = re 49 | .replace_all(&result, |caps: ®ex::Captures| { 50 | let field = &caps[1]; 51 | let value = &caps[2]; 52 | 53 | // Convert based on target platform's not equal operator format 54 | if to_config.operators.not_equal == "!=" { 55 | // Convert to field!="value" format 56 | format!("{}!=\"{}\"", field, value) 57 | } else if to_config.operators.not_equal.trim() == "NOT" { 58 | // Convert to NOT field:"value" format (QUAKE style) 59 | format!("{} {}:\"{}\"", to_config.operators.not_equal, field, value) 60 | } else { 61 | // Other formats 62 | format!("{}{}\"{}\"", field, to_config.operators.not_equal, value) 63 | } 64 | }) 65 | .to_string(); 66 | 67 | // Handle NOT field:"value" format (QUAKE style) 68 | let re_pattern2 = format!(r#"{}\s+(\w+):"([^"]*)""#, not_pattern); 69 | let re2 = Regex::new(&re_pattern2).unwrap(); 70 | 71 | result = re2 72 | .replace_all(&result, |caps: ®ex::Captures| { 73 | let field = &caps[1]; 74 | let value = &caps[2]; 75 | 76 | // Convert based on target platform's not equal operator format 77 | if to_config.operators.not_equal == "!=" { 78 | // Convert to field!="value" format 79 | format!("{}!=\"{}\"", field, value) 80 | } else { 81 | // Other formats 82 | format!("{}{}\"{}\"", field, to_config.operators.not_equal, value) 83 | } 84 | }) 85 | .to_string(); 86 | } 87 | 88 | result 89 | } 90 | 91 | /// Convert other logical operators 92 | pub fn convert_other_operators( 93 | query: &str, 94 | from_config: &PlatformConfig, 95 | to_config: &PlatformConfig, 96 | ) -> String { 97 | let mut result = query.to_string(); 98 | 99 | // Handle other operators 100 | let operator_mappings = [ 101 | (&from_config.operators.equal, &to_config.operators.equal), 102 | (&from_config.operators.and, &to_config.operators.and), 103 | (&from_config.operators.or, &to_config.operators.or), 104 | (&from_config.operators.left_paren, &to_config.operators.left_paren), 105 | (&from_config.operators.right_paren, &to_config.operators.right_paren), 106 | ]; 107 | 108 | for (from_op, to_op) in operator_mappings { 109 | if from_op != to_op { 110 | result = result.replace(from_op, to_op); 111 | } 112 | } 113 | 114 | result 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/converter/query.rs: -------------------------------------------------------------------------------- 1 | use crate::config::ConfigManager; 2 | use crate::converter::{FieldConverter, OperatorConverter, SyntaxValidator}; 3 | use crate::error::ConversionResult; 4 | 5 | /// Main query converter 6 | pub struct QueryConverter { 7 | config_manager: ConfigManager, 8 | } 9 | 10 | impl QueryConverter { 11 | /// Create a new query converter with configuration manager 12 | pub fn new(config_manager: ConfigManager) -> Self { 13 | Self { config_manager } 14 | } 15 | 16 | /// Validate query syntax for the source platform 17 | pub fn validate_query_syntax(&self, query: &str, platform_name: &str) -> ConversionResult<()> { 18 | let from_config = self.config_manager.get_platform_config(platform_name)?; 19 | SyntaxValidator::validate_query_syntax(query, from_config, platform_name) 20 | } 21 | 22 | /// Convert query from one platform to another 23 | pub fn convert( 24 | &self, 25 | query: &str, 26 | from_platform: &str, 27 | to_platform: &str, 28 | ) -> ConversionResult { 29 | let from_config = self.config_manager.get_platform_config(from_platform)?; 30 | let to_config = self.config_manager.get_platform_config(to_platform)?; 31 | 32 | // If same platform, return directly 33 | if from_platform == to_platform { 34 | return Ok(query.to_string()); 35 | } 36 | 37 | // Normalize the query first (convert logical operators to uppercase) 38 | let mut result = SyntaxValidator::normalize_query(query); 39 | 40 | // Convert not equal operators first (needs to be done before field conversion) 41 | result = OperatorConverter::convert_not_equal_operator(&result, from_config, to_config); 42 | 43 | // Convert field prefixes 44 | result = FieldConverter::convert_fields(&result, from_config, to_config); 45 | 46 | // Convert other logical operators 47 | result = OperatorConverter::convert_other_operators(&result, from_config, to_config); 48 | 49 | Ok(result) 50 | } 51 | 52 | /// Get list of supported platforms 53 | pub fn get_supported_platforms(&self) -> Vec { 54 | self.config_manager.get_supported_platforms() 55 | } 56 | 57 | /// Check if a platform is supported 58 | pub fn is_platform_supported(&self, platform: &str) -> bool { 59 | self.config_manager.is_platform_supported(platform) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/converter/validator.rs: -------------------------------------------------------------------------------- 1 | use crate::config::PlatformConfig; 2 | use crate::error::{ConversionError, ConversionResult}; 3 | use regex::Regex; 4 | 5 | /// Syntax validator for query statements 6 | pub struct SyntaxValidator; 7 | 8 | impl SyntaxValidator { 9 | /// Normalize query by converting logical operators to uppercase 10 | pub fn normalize_query(query: &str) -> String { 11 | let mut normalized = query.to_string(); 12 | 13 | // Convert logical operators to uppercase using word boundaries 14 | let replacements = [ 15 | (r"\band\b", "AND"), 16 | (r"\bor\b", "OR"), 17 | (r"\bnot\b", "NOT"), 18 | ]; 19 | 20 | for (pattern, replacement) in replacements { 21 | let re = Regex::new(pattern).unwrap(); 22 | normalized = re.replace_all(&normalized, replacement).to_string(); 23 | } 24 | 25 | normalized 26 | } 27 | 28 | /// Validate query syntax against platform configuration 29 | pub fn validate_query_syntax( 30 | query: &str, 31 | from_config: &PlatformConfig, 32 | platform_name: &str, 33 | ) -> ConversionResult<()> { 34 | // First normalize the query 35 | let normalized_query = Self::normalize_query(query); 36 | 37 | // Check operator consistency (all fields should use the same operator type) 38 | Self::validate_operator_consistency(query, from_config, platform_name)?; 39 | 40 | // Check operator support using normalized query 41 | Self::validate_operators(&normalized_query, from_config, platform_name)?; 42 | 43 | // Check field support using original query (fields are case-sensitive) 44 | Self::validate_fields(query, from_config, platform_name)?; 45 | 46 | Ok(()) 47 | } 48 | 49 | /// Validate operator consistency (all fields should use the same operator type) 50 | fn validate_operator_consistency( 51 | query: &str, 52 | from_config: &PlatformConfig, 53 | platform_name: &str, 54 | ) -> ConversionResult<()> { 55 | let mut used_operators = std::collections::HashSet::new(); 56 | 57 | // Extract all field operators from the query 58 | let re = Regex::new(r"\w+(?:\.\w+)*([=:])").unwrap(); 59 | 60 | for caps in re.captures_iter(query) { 61 | if let Some(op_match) = caps.get(1) { 62 | used_operators.insert(op_match.as_str()); 63 | } 64 | } 65 | 66 | // Check if multiple different operators are used 67 | if used_operators.len() > 1 { 68 | let operators_list: Vec<&str> = used_operators.iter().cloned().collect(); 69 | return Err(ConversionError::SyntaxValidationFailed( 70 | format!("Inconsistent field operators in query. Found: '{}'. {} platform expects consistent use of '{}'", 71 | operators_list.join(", "), 72 | platform_name.to_uppercase(), 73 | from_config.operators.equal) 74 | )); 75 | } 76 | 77 | // Check if the used operator matches the platform's expected operator 78 | if let Some(&used_op) = used_operators.iter().next() { 79 | if used_op != from_config.operators.equal { 80 | return Err(ConversionError::UnsupportedOperator { 81 | platform: platform_name.to_string(), 82 | operator: format!("field{}", used_op), 83 | suggestion: format!("field{}", from_config.operators.equal), 84 | }); 85 | } 86 | } 87 | 88 | Ok(()) 89 | } 90 | 91 | /// Validate operators used in the query (simplified for normalized input) 92 | fn validate_operators( 93 | query: &str, 94 | from_config: &PlatformConfig, 95 | platform_name: &str, 96 | ) -> ConversionResult<()> { 97 | // Check AND operator 98 | if query.contains("AND") && from_config.operators.and != "AND" { 99 | return Err(ConversionError::UnsupportedOperator { 100 | platform: platform_name.to_string(), 101 | operator: "AND".to_string(), 102 | suggestion: from_config.operators.and.clone(), 103 | }); 104 | } 105 | 106 | // Check OR operator 107 | if query.contains("OR") && from_config.operators.or != "OR" { 108 | return Err(ConversionError::UnsupportedOperator { 109 | platform: platform_name.to_string(), 110 | operator: "OR".to_string(), 111 | suggestion: from_config.operators.or.clone(), 112 | }); 113 | } 114 | 115 | // Check && operator 116 | if query.contains("&&") && from_config.operators.and != "&&" { 117 | return Err(ConversionError::UnsupportedOperator { 118 | platform: platform_name.to_string(), 119 | operator: "&&".to_string(), 120 | suggestion: from_config.operators.and.clone(), 121 | }); 122 | } 123 | 124 | // Check || operator 125 | if query.contains("||") && from_config.operators.or != "||" { 126 | return Err(ConversionError::UnsupportedOperator { 127 | platform: platform_name.to_string(), 128 | operator: "||".to_string(), 129 | suggestion: from_config.operators.or.clone(), 130 | }); 131 | } 132 | 133 | // Check NOT operator 134 | let not_re = Regex::new(r"\bNOT\s+\w+").unwrap(); 135 | if not_re.is_match(query) && from_config.operators.not_equal.trim() != "NOT" { 136 | return Err(ConversionError::UnsupportedOperator { 137 | platform: platform_name.to_string(), 138 | operator: "NOT".to_string(), 139 | suggestion: from_config.operators.not_equal.clone(), 140 | }); 141 | } 142 | 143 | // Check != operator 144 | if query.contains("!=") && from_config.operators.not_equal != "!=" { 145 | return Err(ConversionError::UnsupportedOperator { 146 | platform: platform_name.to_string(), 147 | operator: "!=".to_string(), 148 | suggestion: from_config.operators.not_equal.clone(), 149 | }); 150 | } 151 | 152 | Ok(()) 153 | } 154 | 155 | 156 | 157 | /// Validate fields used in the query 158 | fn validate_fields( 159 | query: &str, 160 | from_config: &PlatformConfig, 161 | platform_name: &str, 162 | ) -> ConversionResult<()> { 163 | let used_field_names = Self::extract_field_names_from_query(query); 164 | 165 | for field_name in used_field_names { 166 | // Check if this field name exists in the platform configuration 167 | // First try exact match 168 | if from_config.fields.contains_key(&field_name) { 169 | continue; 170 | } 171 | 172 | // Then try to match by base field name (for compound fields like response.title) 173 | let base_field = if field_name.contains('.') { 174 | field_name.split('.').last().unwrap_or(&field_name) 175 | } else { 176 | &field_name 177 | }; 178 | 179 | // Check if the base field exists in config 180 | let is_supported = from_config.fields.iter().any(|(config_field, _)| { 181 | config_field == base_field 182 | }); 183 | 184 | if !is_supported { 185 | return Err(ConversionError::UnsupportedField { 186 | platform: platform_name.to_string(), 187 | field: base_field.to_string(), 188 | }); 189 | } 190 | } 191 | 192 | Ok(()) 193 | } 194 | 195 | /// Extract field names from query (without operators) 196 | fn extract_field_names_from_query(query: &str) -> Vec { 197 | let mut field_names = Vec::new(); 198 | 199 | // Use regex to find all field names 200 | let re = Regex::new(r"(\w+(?:\.\w+)*)(?:[:=]|!=)").unwrap(); 201 | 202 | for caps in re.captures_iter(query) { 203 | if let Some(field_match) = caps.get(1) { 204 | let field_str = field_match.as_str(); 205 | 206 | if !field_names.contains(&field_str.to_string()) { 207 | field_names.push(field_str.to_string()); 208 | } 209 | } 210 | } 211 | 212 | field_names 213 | } 214 | 215 | 216 | } 217 | -------------------------------------------------------------------------------- /src/error/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod types; 2 | 3 | pub use types::*; 4 | -------------------------------------------------------------------------------- /src/error/types.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | 3 | /// Errors that can occur during query conversion 4 | #[derive(Debug, Clone)] 5 | pub enum ConversionError { 6 | /// Platform is not supported 7 | UnsupportedPlatform(String), 8 | /// Syntax validation failed 9 | SyntaxValidationFailed(String), 10 | /// Field is not supported by the platform 11 | UnsupportedField { platform: String, field: String }, 12 | /// Operator is not supported by the platform 13 | UnsupportedOperator { platform: String, operator: String, suggestion: String }, 14 | /// Configuration loading failed 15 | ConfigurationError(String), 16 | /// Internal conversion error 17 | InternalError(String), 18 | } 19 | 20 | impl fmt::Display for ConversionError { 21 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 22 | match self { 23 | ConversionError::UnsupportedPlatform(platform) => { 24 | write!(f, "Unsupported platform: {}", platform) 25 | } 26 | ConversionError::SyntaxValidationFailed(msg) => { 27 | write!(f, "{}", msg) 28 | } 29 | ConversionError::UnsupportedField { platform, field } => { 30 | write!(f, "{} platform does not support field '{}'", platform.to_uppercase(), field) 31 | } 32 | ConversionError::UnsupportedOperator { platform, operator, suggestion } => { 33 | write!(f, "{} platform does not support '{}' operator, please use '{}' instead", 34 | platform.to_uppercase(), operator, suggestion) 35 | } 36 | ConversionError::ConfigurationError(msg) => { 37 | write!(f, "Configuration error: {}", msg) 38 | } 39 | ConversionError::InternalError(msg) => { 40 | write!(f, "Internal error: {}", msg) 41 | } 42 | } 43 | } 44 | } 45 | 46 | impl std::error::Error for ConversionError {} 47 | 48 | /// Result type for conversion operations 49 | pub type ConversionResult = Result; 50 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod config; 3 | mod converter; 4 | mod error; 5 | mod output; 6 | 7 | pub use cli::Args; 8 | pub use config::ConfigManager; 9 | pub use converter::QueryConverter; 10 | pub use error::ConversionError; 11 | pub use output::OutputFormatter; 12 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | use std::process; 3 | use convertix::{Args, ConfigManager, OutputFormatter, QueryConverter}; 4 | 5 | fn main() { 6 | let args = Args::parse(); 7 | 8 | // Parse query - support reading from file if starts with @ 9 | let query = args.parse_query_input(); 10 | 11 | // Load configuration 12 | let config_manager = match ConfigManager::from_file(&args.config) { 13 | Ok(config) => config, 14 | Err(e) => { 15 | eprintln!("ERROR: Failed to load configuration file: {}", e); 16 | eprintln!("Please ensure configuration file '{}' exists and is properly formatted", args.config.display()); 17 | process::exit(1); 18 | } 19 | }; 20 | 21 | // Create converter 22 | let converter = QueryConverter::new(config_manager); 23 | let supported_platforms = converter.get_supported_platforms(); 24 | 25 | // Validate platform support 26 | if !supported_platforms.contains(&args.platform) { 27 | eprintln!("ERROR: Unsupported platform: {}", args.platform); 28 | eprintln!("Supported platforms: {}", supported_platforms.join(", ")); 29 | process::exit(1); 30 | } 31 | 32 | // Validate query syntax for source platform 33 | if let Err(e) = converter.validate_query_syntax(&query, &args.platform) { 34 | eprintln!("ERROR: {}", e); 35 | process::exit(1); 36 | } 37 | 38 | // Perform conversions 39 | let mut conversions = Vec::new(); 40 | 41 | for target_platform in &supported_platforms { 42 | if target_platform != &args.platform { 43 | // Syntax validation completed at program start, conversion should always succeed here 44 | match converter.convert(&query, &args.platform, target_platform) { 45 | Ok(converted_query) => { 46 | conversions.push((target_platform.clone(), converted_query)); 47 | } 48 | Err(e) => { 49 | // This should theoretically not happen since syntax has been validated 50 | eprintln!("INTERNAL ERROR: Failed to convert to {}: {}", target_platform, e); 51 | process::exit(1); 52 | } 53 | } 54 | } 55 | } 56 | 57 | // Generate and output results 58 | let output_content = OutputFormatter::format_output(&args.format, &args.platform, &query, &conversions); 59 | OutputFormatter::write_output(output_content, args.output); 60 | } 61 | 62 | -------------------------------------------------------------------------------- /src/output/formatter.rs: -------------------------------------------------------------------------------- 1 | use crate::cli::OutputFormat; 2 | use serde_json::json; 3 | use std::fs; 4 | use std::path::PathBuf; 5 | use std::process; 6 | 7 | /// Output formatter for conversion results 8 | pub struct OutputFormatter; 9 | 10 | impl OutputFormatter { 11 | /// Generate raw text output 12 | pub fn generate_raw_output( 13 | platform: &str, 14 | query: &str, 15 | conversions: &[(String, String)], 16 | ) -> String { 17 | let mut output = String::new(); 18 | output.push_str(&format!("Source platform: {}\n", platform)); 19 | output.push_str(&format!("Original query: {}\n", query)); 20 | output.push_str("\n"); 21 | 22 | for (platform, converted_query) in conversions { 23 | output.push_str(&format!( 24 | "[-] {}:\n{}\n\n", 25 | platform.to_uppercase(), 26 | converted_query 27 | )); 28 | } 29 | 30 | output 31 | } 32 | 33 | /// Generate JSON output 34 | pub fn generate_json_output( 35 | source_platform: &str, 36 | query: &str, 37 | conversions: &[(String, String)], 38 | ) -> String { 39 | let mut converted_queries = serde_json::Map::new(); 40 | 41 | for (platform, query) in conversions { 42 | converted_queries.insert(platform.clone(), json!(query)); 43 | } 44 | 45 | let result = json!({ 46 | "source_platform": source_platform, 47 | "original_query": query, 48 | "converted_queries": converted_queries, 49 | }); 50 | 51 | serde_json::to_string_pretty(&result).unwrap_or_else(|e| { 52 | eprintln!("ERROR: Failed to serialize JSON output: {}", e); 53 | process::exit(1); 54 | }) 55 | } 56 | 57 | /// Write output to file or stdout 58 | pub fn write_output(content: String, output_path: Option) { 59 | match output_path { 60 | Some(path) => { 61 | if let Err(e) = fs::write(&path, content) { 62 | eprintln!( 63 | "ERROR: Failed to write to output file '{}': {}", 64 | path.display(), 65 | e 66 | ); 67 | process::exit(1); 68 | } 69 | println!("Output written to: {}", path.display()); 70 | } 71 | None => { 72 | print!("{}", content); 73 | } 74 | } 75 | } 76 | 77 | /// Format output based on the specified format 78 | pub fn format_output( 79 | format: &OutputFormat, 80 | source_platform: &str, 81 | query: &str, 82 | conversions: &[(String, String)], 83 | ) -> String { 84 | match format { 85 | OutputFormat::Raw => Self::generate_raw_output(source_platform, query, conversions), 86 | OutputFormat::Json => Self::generate_json_output(source_platform, query, conversions), 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/output/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod formatter; 2 | 3 | pub use formatter::*; 4 | --------------------------------------------------------------------------------