pub mod soql {
#[must_use]
pub fn escape_string(value: &str) -> String {
let mut escaped = String::with_capacity(value.len() + 16);
for ch in value.chars() {
match ch {
'\'' => escaped.push_str("\\'"),
'\\' => escaped.push_str("\\\\"),
'\n' => escaped.push_str("\\n"),
'\r' => escaped.push_str("\\r"),
'\t' => escaped.push_str("\\t"),
_ => escaped.push(ch),
}
}
escaped
}
#[must_use]
pub fn escape_like(value: &str) -> String {
let base_escaped = escape_string(value);
let mut escaped = String::with_capacity(base_escaped.len() + 8);
for ch in base_escaped.chars() {
match ch {
'%' => escaped.push_str("\\%"),
'_' => escaped.push_str("\\_"),
_ => escaped.push(ch),
}
}
escaped
}
#[must_use]
pub fn is_safe_field_name(name: &str) -> bool {
if name.is_empty() {
return false;
}
let first = name.chars().next().unwrap();
if !first.is_ascii_alphabetic() {
return false;
}
for ch in name.chars() {
if !ch.is_ascii_alphanumeric() && ch != '_' {
return false;
}
}
true
}
pub fn filter_safe_fields<'a>(
fields: impl IntoIterator<Item = &'a str>,
) -> impl Iterator<Item = &'a str> {
fields.into_iter().filter(|f| is_safe_field_name(f))
}
#[must_use]
pub fn is_safe_sobject_name(name: &str) -> bool {
is_safe_field_name(name)
}
#[must_use]
pub fn is_safe_action_name(name: &str) -> bool {
if name.is_empty() {
return false;
}
name.split('.').all(is_safe_field_name)
}
#[must_use]
pub fn build_safe_select(fields: &[&str]) -> Option<String> {
let safe: Vec<_> = filter_safe_fields(fields.iter().copied()).collect();
if safe.is_empty() {
None
} else {
Some(safe.join(", "))
}
}
}
pub mod url {
#[must_use]
pub fn encode_param(value: &str) -> String {
urlencoding::encode(value).into_owned()
}
#[must_use]
pub fn is_valid_salesforce_id(id: &str) -> bool {
let len = id.len();
(len == 15 || len == 18) && id.chars().all(|c| c.is_ascii_alphanumeric())
}
#[must_use]
pub fn sobject_path(sobject: &str, id: &str) -> Option<String> {
use super::soql::is_safe_sobject_name;
if !is_safe_sobject_name(sobject) {
return None;
}
if !is_valid_salesforce_id(id) {
return None;
}
Some(format!("sobjects/{}/{}", sobject, id))
}
}
pub mod xml {
#[must_use]
pub fn escape(value: &str) -> String {
let mut escaped = String::with_capacity(value.len() + 16);
for ch in value.chars() {
match ch {
'&' => escaped.push_str("&"),
'<' => escaped.push_str("<"),
'>' => escaped.push_str(">"),
'"' => escaped.push_str("""),
'\'' => escaped.push_str("'"),
_ => escaped.push(ch),
}
}
escaped
}
}
#[cfg(test)]
mod tests {
use super::*;
mod soql_tests {
use super::soql::*;
#[test]
fn test_escape_string_basic() {
assert_eq!(escape_string("hello"), "hello");
assert_eq!(escape_string("O'Brien"), "O\\'Brien");
assert_eq!(escape_string("test\\path"), "test\\\\path");
}
#[test]
fn test_escape_string_injection_attempts() {
assert_eq!(escape_string("' OR '1'='1"), "\\' OR \\'1\\'=\\'1");
assert_eq!(
escape_string("'; DELETE FROM Account--"),
"\\'; DELETE FROM Account--"
);
assert_eq!(
escape_string("' UNION SELECT Id FROM User--"),
"\\' UNION SELECT Id FROM User--"
);
}
#[test]
fn test_escape_string_special_chars() {
assert_eq!(escape_string("line1\nline2"), "line1\\nline2");
assert_eq!(escape_string("col1\tcol2"), "col1\\tcol2");
assert_eq!(escape_string("text\r\n"), "text\\r\\n");
}
#[test]
fn test_escape_string_mixed() {
assert_eq!(
escape_string("O'Brien's\tfile\\path\n"),
"O\\'Brien\\'s\\tfile\\\\path\\n"
);
}
#[test]
fn test_escape_like() {
assert_eq!(escape_like("100%"), "100\\%");
assert_eq!(escape_like("test_value"), "test\\_value");
assert_eq!(escape_like("O'Brien%"), "O\\'Brien\\%");
}
#[test]
fn test_is_safe_field_name() {
assert!(is_safe_field_name("Id"));
assert!(is_safe_field_name("Name"));
assert!(is_safe_field_name("Custom_Field__c"));
assert!(is_safe_field_name("Account__r"));
assert!(is_safe_field_name("X123"));
assert!(!is_safe_field_name("")); assert!(!is_safe_field_name("123abc")); assert!(!is_safe_field_name("field-name")); assert!(!is_safe_field_name("field.name")); assert!(!is_safe_field_name("field'name")); assert!(!is_safe_field_name("field; DROP")); }
#[test]
fn test_is_safe_action_name() {
assert!(is_safe_action_name("NewCase"));
assert!(is_safe_action_name("LogACall"));
assert!(is_safe_action_name("FeedItem.TextPost"));
assert!(is_safe_action_name("FeedItem.ContentPost"));
assert!(is_safe_action_name("Case.LogACall"));
assert!(!is_safe_action_name("")); assert!(!is_safe_action_name(".")); assert!(!is_safe_action_name(".LeadingDot")); assert!(!is_safe_action_name("TrailingDot.")); assert!(!is_safe_action_name("Bad'; DROP--")); assert!(!is_safe_action_name("Feed.Bad'; DROP--")); }
#[test]
fn test_filter_safe_fields() {
let fields = vec!["Id", "Name", "Bad'; DROP--", "Custom__c", "123start"];
let safe: Vec<_> = filter_safe_fields(fields).collect();
assert_eq!(safe, vec!["Id", "Name", "Custom__c"]);
}
#[test]
fn test_build_safe_select() {
assert_eq!(
build_safe_select(&["Id", "Name", "Email"]),
Some("Id, Name, Email".to_string())
);
assert_eq!(
build_safe_select(&["Id", "Bad'--", "Name"]),
Some("Id, Name".to_string())
);
assert_eq!(build_safe_select(&["Bad'; DROP--"]), None);
}
}
mod url_tests {
use super::url::*;
#[test]
fn test_encode_param() {
assert_eq!(encode_param("simple"), "simple");
assert_eq!(encode_param("has space"), "has%20space");
assert_eq!(encode_param("path/traversal"), "path%2Ftraversal");
assert_eq!(encode_param("../../etc/passwd"), "..%2F..%2Fetc%2Fpasswd");
}
#[test]
fn test_is_valid_salesforce_id() {
assert!(is_valid_salesforce_id("001000000000001"));
assert!(is_valid_salesforce_id("001000000000001AAA"));
assert!(is_valid_salesforce_id("001Abc000000XYZ"));
assert!(!is_valid_salesforce_id("")); assert!(!is_valid_salesforce_id("short")); assert!(!is_valid_salesforce_id("001/../../etc/passwd")); assert!(!is_valid_salesforce_id("001000000000001!")); }
#[test]
fn test_sobject_path() {
assert_eq!(
sobject_path("Account", "001000000000001"),
Some("sobjects/Account/001000000000001".to_string())
);
assert_eq!(
sobject_path("Custom__c", "a00000000000001AAA"),
Some("sobjects/Custom__c/a00000000000001AAA".to_string())
);
assert_eq!(sobject_path("Bad'; DROP--", "001000000000001"), None);
assert_eq!(sobject_path("Account", "../../etc/passwd"), None);
}
}
mod xml_tests {
use super::xml::*;
#[test]
fn test_escape() {
assert_eq!(escape("hello"), "hello");
assert_eq!(escape("<tag>"), "<tag>");
assert_eq!(escape("&"), "&amp;");
assert_eq!(escape("\"quoted\""), ""quoted"");
assert_eq!(escape("it's"), "it's");
assert_eq!(
escape("<script>alert('xss')</script>"),
"<script>alert('xss')</script>"
);
}
}
}