use std::collections::HashMap;
use base64::Engine;
use flate2::read::GzDecoder;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::command::Command;
use crate::error_code::ErrorCode;
use crate::language::Language;
use crate::new_line::NewLine;
pub const DEFAULT_API_URL: &str = "https://www.stringencrypt.com/api.php";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ApiResponse {
#[serde(default)]
pub error: Option<i64>,
#[serde(default)]
pub source: Option<String>,
#[serde(default)]
pub expired: Option<bool>,
#[serde(default)]
pub credits_left: Option<i64>,
#[serde(default)]
pub credits_total: Option<i64>,
#[serde(default)]
pub demo: Option<bool>,
#[serde(default)]
pub label_limit: Option<i64>,
#[serde(default)]
pub string_limit: Option<i64>,
#[serde(default)]
pub bytes_limit: Option<i64>,
#[serde(default)]
pub cmd_min: Option<i64>,
#[serde(default)]
pub cmd_max: Option<i64>,
}
pub struct StringEncrypt {
prefer_curl: bool,
decompress_encrypt_source: bool,
command: Option<String>,
api_key: String,
label: String,
input_string: Option<String>,
input_bytes: Option<Vec<u8>>,
compression: bool,
language: String,
highlight: Highlight,
cmd_min: i32,
cmd_max: i32,
local: bool,
unicode: bool,
lang_locale: String,
ansi_encoding: String,
new_lines: String,
template: Option<String>,
return_template: bool,
include_tags: bool,
include_example: bool,
include_debug_comments: bool,
client: reqwest::Client,
api_url: String,
}
#[derive(Clone)]
enum Highlight {
Off,
On(String),
}
impl StringEncrypt {
pub fn new(api_key: impl Into<String>, prefer_curl: bool) -> Self {
Self {
prefer_curl,
decompress_encrypt_source: true,
command: None,
api_key: api_key.into(),
label: "Label".to_string(),
input_string: None,
input_bytes: None,
compression: false,
language: Language::PHP.to_string(),
highlight: Highlight::Off,
cmd_min: 1,
cmd_max: 3,
local: false,
unicode: true,
lang_locale: "en_US.utf8".to_string(),
ansi_encoding: "WINDOWS-1250".to_string(),
new_lines: NewLine::LF.to_string(),
template: None,
return_template: false,
include_tags: false,
include_example: false,
include_debug_comments: false,
client: reqwest::Client::new(),
api_url: DEFAULT_API_URL.to_string(),
}
}
pub fn demo() -> Self {
Self::new("", false)
}
pub fn prefer_curl(&self) -> bool {
self.prefer_curl
}
pub fn set_prefer_curl(&mut self, prefer_curl: bool) -> &mut Self {
self.prefer_curl = prefer_curl;
self
}
pub fn decompress_encrypt_source(&self) -> bool {
self.decompress_encrypt_source
}
pub fn set_decompress_encrypt_source(&mut self, decompress: bool) -> &mut Self {
self.decompress_encrypt_source = decompress;
self
}
pub fn command(&self) -> Option<&str> {
self.command.as_deref()
}
pub fn set_command(&mut self, command: &str) -> &mut Self {
self.command = Some(command.to_string());
self
}
pub fn label(&self) -> &str {
&self.label
}
pub fn set_label(&mut self, label: impl Into<String>) -> &mut Self {
self.label = label.into();
self
}
pub fn set_string(&mut self, string: impl Into<String>) -> &mut Self {
self.input_string = Some(string.into());
self.input_bytes = None;
self
}
pub fn set_bytes(&mut self, bytes: impl Into<Vec<u8>>) -> &mut Self {
self.input_bytes = Some(bytes.into());
self.input_string = None;
self
}
pub fn compression(&self) -> bool {
self.compression
}
pub fn set_compression(&mut self, compression: bool) -> &mut Self {
self.compression = compression;
self
}
pub fn language(&self) -> &str {
&self.language
}
pub fn set_language(&mut self, language: &str) -> &mut Self {
self.language = language.to_string();
self
}
pub fn highlight_off(&self) -> bool {
matches!(self.highlight, Highlight::Off)
}
pub fn highlight_mode(&self) -> Option<&str> {
match &self.highlight {
Highlight::Off => None,
Highlight::On(s) => Some(s.as_str()),
}
}
pub fn set_highlight(&mut self, highlight: impl Into<HighlightArg>) -> &mut Self {
self.highlight = match highlight.into() {
HighlightArg::Off => Highlight::Off,
HighlightArg::On(s) => Highlight::On(s),
};
self
}
pub fn cmd_min(&self) -> i32 {
self.cmd_min
}
pub fn set_cmd_min(&mut self, cmd_min: i32) -> &mut Self {
self.cmd_min = cmd_min;
self
}
pub fn cmd_max(&self) -> i32 {
self.cmd_max
}
pub fn set_cmd_max(&mut self, cmd_max: i32) -> &mut Self {
self.cmd_max = cmd_max;
self
}
pub fn local(&self) -> bool {
self.local
}
pub fn set_local(&mut self, local: bool) -> &mut Self {
self.local = local;
self
}
pub fn unicode(&self) -> bool {
self.unicode
}
pub fn set_unicode(&mut self, unicode: bool) -> &mut Self {
self.unicode = unicode;
self
}
pub fn lang_locale(&self) -> &str {
&self.lang_locale
}
pub fn set_lang_locale(&mut self, lang_locale: impl Into<String>) -> &mut Self {
self.lang_locale = lang_locale.into();
self
}
pub fn ansi_encoding(&self) -> &str {
&self.ansi_encoding
}
pub fn set_ansi_encoding(&mut self, ansi_encoding: impl Into<String>) -> &mut Self {
self.ansi_encoding = ansi_encoding.into();
self
}
pub fn new_lines(&self) -> &str {
&self.new_lines
}
pub fn set_new_lines(&mut self, new_lines: &str) -> &mut Self {
self.new_lines = new_lines.to_string();
self
}
pub fn template(&self) -> Option<&str> {
self.template.as_deref()
}
pub fn set_template(&mut self, template: Option<String>) -> &mut Self {
self.template = template;
self
}
pub fn return_template(&self) -> bool {
self.return_template
}
pub fn set_return_template(&mut self, return_template: bool) -> &mut Self {
self.return_template = return_template;
self
}
pub fn include_tags(&self) -> bool {
self.include_tags
}
pub fn set_include_tags(&mut self, include_tags: bool) -> &mut Self {
self.include_tags = include_tags;
self
}
pub fn include_example(&self) -> bool {
self.include_example
}
pub fn set_include_example(&mut self, include_example: bool) -> &mut Self {
self.include_example = include_example;
self
}
pub fn include_debug_comments(&self) -> bool {
self.include_debug_comments
}
pub fn set_include_debug_comments(&mut self, include_debug_comments: bool) -> &mut Self {
self.include_debug_comments = include_debug_comments;
self
}
pub fn api_url(&self) -> &str {
&self.api_url
}
pub fn set_api_url(&mut self, url: impl Into<String>) -> &mut Self {
self.api_url = url.into();
self
}
pub fn reset(&mut self) -> &mut Self {
self.command = None;
self.label = "$label".to_string();
self.input_string = None;
self.input_bytes = None;
self.compression = false;
self.language = Language::PHP.to_string();
self.highlight = Highlight::Off;
self.cmd_min = 1;
self.cmd_max = 3;
self.local = false;
self.unicode = true;
self.lang_locale = "en_US.utf8".to_string();
self.ansi_encoding = "WINDOWS-1250".to_string();
self.new_lines = NewLine::LF.to_string();
self.template = None;
self.return_template = false;
self.include_tags = false;
self.include_example = false;
self.include_debug_comments = false;
self
}
pub async fn is_demo(&mut self) -> Option<ApiResponse> {
let previous = self.command.clone();
self.set_command(Command::IS_DEMO);
let result = self.send().await;
self.command = previous;
result
}
pub async fn encrypt_file_contents(
&mut self,
file_path: impl AsRef<std::path::Path>,
label: &str,
) -> Option<ApiResponse> {
let raw = match std::fs::read(file_path.as_ref()) {
Ok(b) => b,
Err(_) => return None,
};
if raw.is_empty() {
return None;
}
let saved = SavedState {
command: self.command.clone(),
input_string: self.input_string.clone(),
input_bytes: self.input_bytes.clone(),
label: self.label.clone(),
};
self.set_command(Command::ENCRYPT)
.set_bytes(raw)
.set_label(label);
let result = self.send().await;
self.command = saved.command;
self.input_string = saved.input_string;
self.input_bytes = saved.input_bytes;
self.label = saved.label;
result
}
pub async fn encrypt_string(
&mut self,
string: &str,
label: &str,
) -> Option<ApiResponse> {
let saved = SavedState {
command: self.command.clone(),
input_string: self.input_string.clone(),
input_bytes: self.input_bytes.clone(),
label: self.label.clone(),
};
self.set_command(Command::ENCRYPT)
.set_string(string)
.set_label(label);
let result = self.send().await;
self.command = saved.command;
self.input_string = saved.input_string;
self.input_bytes = saved.input_bytes;
self.label = saved.label;
result
}
fn to_request_map(&self) -> Result<HashMap<String, RequestValue>, &'static str> {
let cmd = self.command.as_deref().ok_or("Command must be set (use set_command()).")?;
if cmd == Command::INFO {
return Ok(self.build_info_params());
}
if cmd == Command::IS_DEMO {
return Ok(self.build_is_demo_params());
}
if cmd == Command::ENCRYPT {
return Ok(self.build_encrypt_params());
}
Err("Unknown command.")
}
pub async fn send(&mut self) -> Option<ApiResponse> {
let params = match self.to_request_map() {
Ok(m) => m,
Err(_) => return None,
};
let body = form_encode_params(¶ms);
let raw = self.post(&self.api_url, &body).await?;
if raw.is_empty() {
return None;
}
let mut decoded: Value = serde_json::from_str(&raw).ok()?;
if !decoded.is_object() {
return None;
}
let obj = decoded.as_object_mut()?;
self.apply_decryptor_source_decompression(obj);
serde_json::from_value(Value::Object(std::mem::take(obj))).ok()
}
fn apply_decryptor_source_decompression(&self, response: &mut serde_json::Map<String, Value>) {
if self.command.as_deref() != Some(Command::ENCRYPT)
|| !self.compression
|| !self.decompress_encrypt_source
{
return;
}
let err = response.get("error").and_then(|v| v.as_i64()).unwrap_or(-1);
if err != ErrorCode::SUCCESS as i64 {
return;
}
let Some(Value::String(src)) = response.get("source").cloned() else {
return;
};
let Ok(binary) = base64::engine::general_purpose::STANDARD.decode(src.as_bytes()) else {
return;
};
let mut decoder = GzDecoder::new(&binary[..]);
let mut plain = String::new();
if std::io::Read::read_to_string(&mut decoder, &mut plain).is_err() {
return;
}
response.insert("source".to_string(), Value::String(plain));
}
fn build_info_params(&self) -> HashMap<String, RequestValue> {
let mut p = HashMap::new();
p.insert("command".to_string(), RequestValue::String(Command::INFO.to_string()));
p.insert("code".to_string(), RequestValue::String(self.api_key.clone()));
p
}
fn build_is_demo_params(&self) -> HashMap<String, RequestValue> {
let mut p = HashMap::new();
p.insert("command".to_string(), RequestValue::String(Command::IS_DEMO.to_string()));
p.insert("code".to_string(), RequestValue::String(self.api_key.clone()));
p
}
fn build_encrypt_params(&self) -> HashMap<String, RequestValue> {
let mut p = HashMap::new();
p.insert("command".to_string(), RequestValue::String(Command::ENCRYPT.to_string()));
p.insert("code".to_string(), RequestValue::String(self.api_key.clone()));
p.insert("label".to_string(), RequestValue::String(self.label.clone()));
p.insert("compression".to_string(), RequestValue::Bool(self.compression));
p.insert("lang".to_string(), RequestValue::String(self.language.clone()));
p.insert("cmd_min".to_string(), RequestValue::I32(self.cmd_min));
p.insert("cmd_max".to_string(), RequestValue::I32(self.cmd_max));
p.insert("local".to_string(), RequestValue::Bool(self.local));
p.insert("unicode".to_string(), RequestValue::Bool(self.unicode));
p.insert("lang_locale".to_string(), RequestValue::String(self.lang_locale.clone()));
p.insert("ansi_encoding".to_string(), RequestValue::String(self.ansi_encoding.clone()));
p.insert("new_lines".to_string(), RequestValue::String(self.new_lines.clone()));
p.insert(
"return_template".to_string(),
RequestValue::Bool(self.return_template),
);
p.insert("include_tags".to_string(), RequestValue::Bool(self.include_tags));
p.insert(
"include_example".to_string(),
RequestValue::Bool(self.include_example),
);
p.insert(
"include_debug_comments".to_string(),
RequestValue::Bool(self.include_debug_comments),
);
if let Some(ref s) = self.input_string {
p.insert("string".to_string(), RequestValue::String(s.clone()));
} else if let Some(ref b) = self.input_bytes {
p.insert("bytes".to_string(), RequestValue::Bytes(b.clone()));
}
if let Highlight::On(ref mode) = self.highlight {
p.insert("highlight".to_string(), RequestValue::String(mode.clone()));
}
if let Some(ref t) = self.template {
p.insert("template".to_string(), RequestValue::String(t.clone()));
}
p
}
async fn post(&self, url: &str, body: &str) -> Option<String> {
let res = self
.client
.post(url)
.header("Content-Type", "application/x-www-form-urlencoded")
.header(
"User-Agent",
"pelock/stringencrypt (+https://www.stringencrypt.com)",
)
.body(body.to_string())
.send()
.await
.ok()?;
res.text().await.ok()
}
}
struct SavedState {
command: Option<String>,
input_string: Option<String>,
input_bytes: Option<Vec<u8>>,
label: String,
}
pub enum HighlightArg {
Off,
On(String),
}
impl From<bool> for HighlightArg {
fn from(value: bool) -> Self {
if value {
HighlightArg::On(String::new())
} else {
HighlightArg::Off
}
}
}
impl From<&'static str> for HighlightArg {
fn from(s: &'static str) -> Self {
HighlightArg::On(s.to_string())
}
}
impl From<String> for HighlightArg {
fn from(s: String) -> Self {
HighlightArg::On(s)
}
}
#[derive(Clone)]
enum RequestValue {
String(String),
Bool(bool),
I32(i32),
Bytes(Vec<u8>),
}
fn bytes_to_latin1(bytes: &[u8]) -> String {
bytes
.iter()
.map(|&b| char::from_u32(u32::from(b)).unwrap_or('\u{FFFD}'))
.collect()
}
fn form_encode_params(data: &HashMap<String, RequestValue>) -> String {
let mut ser = url::form_urlencoded::Serializer::new(String::new());
let mut keys: Vec<&String> = data.keys().collect();
keys.sort();
for k in keys {
let v = match &data[k] {
RequestValue::Bool(b) => {
if *b {
"1".to_string()
} else {
"0".to_string()
}
}
RequestValue::Bytes(buf) => bytes_to_latin1(buf),
RequestValue::I32(n) => n.to_string(),
RequestValue::String(s) => s.clone(),
};
ser.append_pair(k, &v);
}
ser.finish()
}