use core::fmt;
use std::collections::HashMap;
use std::path::Path;
use hurl_core::types::Count;
use crate::runner::Output;
use crate::util::path::ContextDir;
use super::cookie_store::CookieStore;
use super::header::{CONTENT_TYPE, Header, HeaderVec};
use super::options::ClientOptions;
use super::param::Param;
use super::request::{CredentialForwarding, FollowLocation, IpResolve, RequestedHttpVersion};
use super::request_spec::{Body, FileParam, Method, MultipartParam, RequestSpec};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct CurlCmd {
args: Vec<String>,
}
impl fmt::Display for CurlCmd {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.args.join(" "))
}
}
impl Default for CurlCmd {
fn default() -> Self {
CurlCmd {
args: vec!["curl".to_string()],
}
}
}
impl CurlCmd {
pub fn new(
request_spec: &RequestSpec,
cookie_store: &CookieStore,
context_dir: &ContextDir,
output: Option<&Output>,
options: &ClientOptions,
) -> Self {
let mut args = vec!["curl".to_string()];
let mut params = method_params(request_spec, options.follow_location);
args.append(&mut params);
let mut headers = request_spec.headers.clone();
headers.extend(&options.headers);
let mut params = headers_params(
&headers,
request_spec.implicit_content_type.as_deref(),
&request_spec.body,
);
args.append(&mut params);
let mut params = body_params(request_spec, context_dir);
args.append(&mut params);
let mut params = cookies_params(request_spec, cookie_store);
args.append(&mut params);
let mut params = other_options_params(context_dir, output, options);
args.append(&mut params);
let mut params = url_param(request_spec);
args.append(&mut params);
CurlCmd { args }
}
}
fn method_params(request_spec: &RequestSpec, follow_location: FollowLocation) -> Vec<String> {
let has_body = !request_spec.multipart.is_empty()
|| !request_spec.form.is_empty()
|| !request_spec.body.bytes().is_empty();
request_spec.method.curl_args(has_body, follow_location)
}
fn headers_params(
headers: &HeaderVec,
implicit_content_type: Option<&str>,
body: &Body,
) -> Vec<String> {
let mut args = vec![];
for header in headers.iter() {
args.append(&mut header.curl_args());
}
let has_explicit_content_type = headers.contains_key(CONTENT_TYPE);
if has_explicit_content_type {
return args;
}
if let Some(content_type) = implicit_content_type {
if content_type != "application/x-www-form-urlencoded"
&& content_type != "multipart/form-data"
{
args.push("--header".to_string());
args.push(format!("'{CONTENT_TYPE}: {content_type}'"));
}
} else if !body.bytes().is_empty() {
match body {
Body::Text(_) => {
args.push("--header".to_string());
args.push(format!("'{CONTENT_TYPE}:'"));
}
Body::Binary(_) => {
args.push("--header".to_string());
args.push(format!("'{CONTENT_TYPE}: application/octet-stream'"));
}
Body::File(_, _) => {
args.push("--header".to_string());
args.push(format!("'{CONTENT_TYPE}:'"));
}
}
}
args
}
fn body_params(request_spec: &RequestSpec, context_dir: &ContextDir) -> Vec<String> {
let mut args = vec![];
for param in request_spec.form.iter() {
args.push("--data".to_string());
args.push(format!("'{}'", param.curl_arg_escape()));
}
for param in request_spec.multipart.iter() {
args.push("--form".to_string());
args.push(format!("'{}'", param.curl_arg(context_dir)));
}
if request_spec.body.bytes().is_empty() {
return args;
}
let param = match request_spec.body {
Body::File(_, _) => "--data-binary",
_ => "--data",
};
args.push(param.to_string());
args.push(request_spec.body.curl_arg(context_dir));
args
}
fn cookies_params(request_spec: &RequestSpec, cookie_store: &CookieStore) -> Vec<String> {
let mut cookies_from_req = request_spec
.cookies
.iter()
.map(|c| (c.name.as_str(), c.value.as_str()))
.collect::<Vec<_>>();
let mut cookies_from_store = cookie_store
.cookies()
.filter(|c| !c.is_expired())
.filter(|c| c.match_domain(&request_spec.url))
.map(|c| (c.name(), c.value()))
.collect::<Vec<_>>();
let mut all_cookies = vec![];
all_cookies.append(&mut cookies_from_req);
all_cookies.append(&mut cookies_from_store);
if all_cookies.is_empty() {
return vec![];
}
let mut args = vec![];
args.push("--cookie".to_string());
let value = all_cookies
.iter()
.map(|(name, value)| format!("{name}={value}"))
.collect::<Vec<String>>()
.join("; ");
args.push(encode_shell_string(&value));
args
}
fn other_options_params(
context_dir: &ContextDir,
output: Option<&Output>,
options: &ClientOptions,
) -> Vec<String> {
let mut args = options.curl_args();
match output {
Some(Output::File(filename)) => {
let filename = context_dir.resolved_path(filename);
args.push("--output".to_string());
args.push(filename.to_string_lossy().to_string());
}
Some(Output::Stdout) => {
args.push("--output".to_string());
args.push("-".to_string());
}
None => {}
}
args
}
fn url_param(request_spec: &RequestSpec) -> Vec<String> {
let mut args = vec![];
let querystring = if request_spec.querystring.is_empty() {
String::new()
} else {
let params = request_spec
.querystring
.iter()
.map(|p| p.curl_arg_escape())
.collect::<Vec<String>>();
params.join("&")
};
let url = if querystring.as_str() == "" {
request_spec.url.raw()
} else if request_spec.url.raw().contains('?') {
format!("{}&{}", request_spec.url.raw(), querystring)
} else {
format!("{}?{}", request_spec.url.raw(), querystring)
};
let url = format!("'{url}'");
if url.contains('{') || url.contains('}') || url.contains('[') || url.contains(']') {
args.push("--globoff".to_string());
}
args.push(url);
args
}
fn encode_byte(b: u8) -> String {
format!("\\x{b:02x}")
}
fn encode_bytes(bytes: &[u8]) -> String {
bytes.iter().map(|b| encode_byte(*b)).collect()
}
impl Method {
fn curl_args(&self, has_body: bool, follow_location: FollowLocation) -> Vec<String> {
match self.0.as_str() {
"GET" => {
if has_body {
vec!["--request".to_string(), "GET".to_string()]
} else {
vec![]
}
}
"HEAD" => vec!["--head".to_string()],
"POST" => {
if has_body {
vec![]
} else if matches!(follow_location, FollowLocation::Follow(_)) {
vec!["--data".to_string(), "''".to_string()]
} else {
vec!["--request".to_string(), "POST".to_string()]
}
}
s => vec!["--request".to_string(), s.to_string()],
}
}
}
impl Header {
fn curl_args(&self) -> Vec<String> {
let name = &self.name;
let value = &self.value;
vec![
"--header".to_string(),
if self.value.is_empty() {
encode_shell_string(&format!("{name};"))
} else {
encode_shell_string(&format!("{name}: {value}"))
},
]
}
}
impl Param {
fn curl_arg_escape(&self) -> String {
let name = &self.name;
let value = escape_url(&self.value);
format!("{name}={value}")
}
fn curl_arg(&self) -> String {
let name = &self.name;
let value = &self.value;
format!("{name}={value}")
}
}
impl MultipartParam {
fn curl_arg(&self, context_dir: &ContextDir) -> String {
match self {
MultipartParam::Param(param) => param.curl_arg(),
MultipartParam::FileParam(FileParam {
name,
filename,
content_type,
..
}) => {
let path = context_dir.resolved_path(Path::new(filename));
let value = format!("@{};type={}", path.to_string_lossy(), content_type);
format!("{name}={value}")
}
}
}
}
impl Body {
fn curl_arg(&self, context_dir: &ContextDir) -> String {
match self {
Body::Text(s) => encode_shell_string(s),
Body::Binary(bytes) => format!("$'{}'", encode_bytes(bytes)),
Body::File(_, filename) => {
let path = context_dir.resolved_path(Path::new(filename));
format!("'@{}'", path.to_string_lossy())
}
}
}
}
impl ClientOptions {
fn curl_args(&self) -> Vec<String> {
let mut arguments = vec![];
if let Some(ref aws_sigv4) = self.aws_sigv4 {
arguments.push("--aws-sigv4".to_string());
arguments.push(aws_sigv4.clone());
}
if let Some(ref cacert_file) = self.cacert_file {
arguments.push("--cacert".to_string());
arguments.push(cacert_file.clone());
}
if let Some(ref client_cert_file) = self.client_cert_file {
arguments.push("--cert".to_string());
arguments.push(client_cert_file.clone());
}
if let Some(ref client_key_file) = self.client_key_file {
arguments.push("--key".to_string());
arguments.push(client_key_file.clone());
}
if self.compressed {
arguments.push("--compressed".to_string());
}
if self.connect_timeout != ClientOptions::default().connect_timeout {
arguments.push("--connect-timeout".to_string());
arguments.push(self.connect_timeout.as_secs().to_string());
}
for connect in self.connects_to.iter() {
arguments.push("--connect-to".to_string());
arguments.push(connect.clone());
}
if let Some(ref cookie_file) = self.cookie_input_file {
arguments.push("--cookie".to_string());
arguments.push(cookie_file.clone());
}
if self.digest {
arguments.push("--digest".to_string());
}
match self.http_version {
RequestedHttpVersion::Default => {}
RequestedHttpVersion::Http10 => arguments.push("--http1.0".to_string()),
RequestedHttpVersion::Http11 => arguments.push("--http1.1".to_string()),
RequestedHttpVersion::Http2 => arguments.push("--http2".to_string()),
RequestedHttpVersion::Http3 => arguments.push("--http3".to_string()),
}
if self.insecure {
arguments.push("--insecure".to_string());
}
match self.ip_resolve {
IpResolve::Default => {}
IpResolve::IpV4 => arguments.push("--ipv4".to_string()),
IpResolve::IpV6 => arguments.push("--ipv6".to_string()),
}
match self.follow_location {
FollowLocation::Follow(CredentialForwarding::OnlyInitialHost) => {
arguments.push("--location".to_string());
}
FollowLocation::Follow(CredentialForwarding::AllHosts) => {
arguments.push("--location-trusted".to_string());
}
FollowLocation::No => {}
}
if let Some(max_filesize) = self.max_filesize {
arguments.push("--max-filesize".to_string());
arguments.push(max_filesize.to_string());
}
if let Some(max_speed) = self.max_recv_speed {
arguments.push("--limit-rate".to_string());
arguments.push(max_speed.to_string());
}
if self.max_redirect != ClientOptions::default().max_redirect {
let max_redirect = match self.max_redirect {
Count::Finite(n) => n as i32,
Count::Infinite => -1,
};
arguments.push("--max-redirs".to_string());
arguments.push(max_redirect.to_string());
}
if self.timeout != ClientOptions::default().timeout {
arguments.push("--max-time".to_string());
arguments.push(self.timeout.as_secs().to_string());
}
if self.negotiate {
arguments.push("--negotiate".to_string());
}
if let Some(filename) = &self.netrc_file {
arguments.push("--netrc-file".to_string());
arguments.push(format!("'{filename}'"));
}
if self.netrc_optional {
arguments.push("--netrc-optional".to_string());
}
if self.netrc {
arguments.push("--netrc".to_string());
}
if self.ntlm {
arguments.push("--ntlm".to_string());
}
if self.path_as_is {
arguments.push("--path-as-is".to_string());
}
if let Some(ref pinned_pub_key) = self.pinned_pub_key {
arguments.push("--pinnedpubkey".to_string());
arguments.push(pinned_pub_key.clone());
}
if let Some(ref proxy) = self.proxy {
arguments.push("--proxy".to_string());
arguments.push(format!("'{proxy}'"));
}
for resolve in self.resolves.iter() {
arguments.push("--resolve".to_string());
arguments.push(resolve.clone());
}
if self.ssl_no_revoke {
arguments.push("--ssl-no-revoke".to_string());
}
if let Some(ref unix_socket) = self.unix_socket {
arguments.push("--unix-socket".to_string());
arguments.push(format!("'{unix_socket}'"));
}
if let Some(ref user) = self.user {
arguments.push("--user".to_string());
arguments.push(format!("'{user}'"));
}
if let Some(ref user_agent) = self.user_agent {
arguments.push("--user-agent".to_string());
arguments.push(format!("'{user_agent}'"));
}
arguments
}
}
fn escape_url(s: &str) -> String {
percent_encoding::percent_encode(s.as_bytes(), percent_encoding::NON_ALPHANUMERIC).to_string()
}
fn encode_shell_string(s: &str) -> String {
if escape_mode(s) {
let escaped = escape_string(s);
format!("$'{escaped}'")
} else {
format!("'{s}'")
}
}
fn escape_mode(s: &str) -> bool {
for c in s.chars() {
if c == '\n' || c == '\t' || c == '\'' {
return true;
}
}
false
}
fn escape_string(s: &str) -> String {
let mut escaped_sequences = HashMap::new();
escaped_sequences.insert('\n', "\\n");
escaped_sequences.insert('\t', "\\t");
escaped_sequences.insert('\'', "\\'");
escaped_sequences.insert('\\', "\\\\");
let mut escaped = String::new();
for c in s.chars() {
match escaped_sequences.get(&c) {
None => escaped.push(c),
Some(escaped_seq) => escaped.push_str(escaped_seq),
}
}
escaped
}
#[cfg(test)]
mod tests {
use std::path::Path;
use std::str::FromStr;
use std::time::Duration;
use hurl_core::types::BytesPerSec;
use super::*;
use crate::http::{HeaderVec, Url};
#[test]
fn hello_request_with_default_options() {
let mut request = RequestSpec {
method: Method("GET".to_string()),
url: Url::from_str("http://localhost:8000/hello").unwrap(),
..Default::default()
};
let context_dir = ContextDir::default();
let cookie_store = CookieStore::new();
let options = ClientOptions::default();
let output = None;
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(cmd.to_string(), "curl 'http://localhost:8000/hello'");
let output = Some(Output::new("foo.out"));
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl \
--output foo.out \
'http://localhost:8000/hello'"
);
let mut headers = HeaderVec::new();
headers.push(Header::new("User-Agent", "iPhone"));
headers.push(Header::new("Foo", "Bar"));
request.headers = headers;
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl \
--header 'User-Agent: iPhone' \
--header 'Foo: Bar' \
--output foo.out \
'http://localhost:8000/hello'"
);
let mut cookie_store = CookieStore::new();
cookie_store
.add_cookie("localhost TRUE / FALSE 0 cookie1 valueA")
.unwrap();
cookie_store
.add_cookie("localhost FALSE / FALSE 1 cookie2 valueB")
.unwrap();
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl \
--header 'User-Agent: iPhone' \
--header 'Foo: Bar' \
--cookie 'cookie1=valueA' \
--output foo.out \
'http://localhost:8000/hello'"
);
}
#[test]
fn hello_request_with_options() {
let request = RequestSpec {
method: Method("GET".to_string()),
url: Url::from_str("http://localhost:8000/hello").unwrap(),
..Default::default()
};
let context_dir = ContextDir::default();
let cookie_store = CookieStore::new();
let mut headers = HeaderVec::new();
headers.push(Header::new("Test-Header-1", "content-1"));
headers.push(Header::new("Test-Header-2", "content-2"));
headers.push(Header::new("Test-Header-Empty", ""));
let options = ClientOptions {
allow_reuse: true,
aws_sigv4: None,
cacert_file: None,
client_cert_file: None,
client_key_file: None,
compressed: true,
connect_timeout: Duration::from_secs(20),
connects_to: vec!["example.com:443:host-47.example.com:443".to_string()],
cookie_input_file: Some("cookie_file".to_string()),
digest: false,
follow_location: FollowLocation::Follow(CredentialForwarding::OnlyInitialHost),
headers,
http_version: RequestedHttpVersion::Http10,
insecure: true,
ip_resolve: IpResolve::IpV6,
max_filesize: None,
max_recv_speed: Some(BytesPerSec(8000)),
max_redirect: Count::Finite(10),
max_send_speed: Some(BytesPerSec(8000)),
negotiate: true,
netrc: false,
netrc_file: Some("/var/run/netrc".to_string()),
netrc_optional: true,
ntlm: true,
path_as_is: true,
pinned_pub_key: None,
proxy: Some("localhost:3128".to_string()),
no_proxy: None,
resolves: vec![
"foo.com:80:192.168.0.1".to_string(),
"bar.com:443:127.0.0.1".to_string(),
],
ssl_no_revoke: false,
timeout: Duration::from_secs(10),
unix_socket: Some("/var/run/example.sock".to_string()),
use_cookie_store: true,
user: Some("user:password".to_string()),
user_agent: Some("my-useragent".to_string()),
verbosity: None,
};
let cmd = CurlCmd::new(&request, &cookie_store, &context_dir, None, &options);
assert_eq!(
cmd.to_string(),
"curl \
--header 'Test-Header-1: content-1' \
--header 'Test-Header-2: content-2' \
--header 'Test-Header-Empty;' \
--compressed \
--connect-timeout 20 \
--connect-to example.com:443:host-47.example.com:443 \
--cookie cookie_file \
--http1.0 \
--insecure \
--ipv6 \
--location \
--limit-rate 8000 \
--max-redirs 10 \
--max-time 10 \
--negotiate \
--netrc-file '/var/run/netrc' \
--netrc-optional \
--ntlm \
--path-as-is \
--proxy 'localhost:3128' \
--resolve foo.com:80:192.168.0.1 \
--resolve bar.com:443:127.0.0.1 \
--unix-socket '/var/run/example.sock' \
--user 'user:password' \
--user-agent 'my-useragent' \
'http://localhost:8000/hello'"
);
}
#[test]
fn url_with_dot() {
let request = RequestSpec {
method: Method("GET".to_string()),
url: Url::from_str("https://example.org/hello/../to/../your/../file").unwrap(),
..Default::default()
};
let context_dir = ContextDir::default();
let cookies = CookieStore::new();
let options = ClientOptions::default();
let output = None;
let cmd = CurlCmd::new(&request, &cookies, &context_dir, output.as_ref(), &options);
assert_eq!(
cmd.to_string(),
"curl 'https://example.org/hello/../to/../your/../file'"
);
}
#[test]
fn url_with_curl_glob() {
let request = RequestSpec {
method: Method("GET".to_string()),
url: Url::from_str("http://foo.com?param1=value1¶m2={bar}").unwrap(),
..Default::default()
};
let context_dir = ContextDir::default();
let cookie_store = CookieStore::new();
let options = ClientOptions::default();
let output = None;
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl \
--globoff \
'http://foo.com?param1=value1¶m2={bar}'"
);
}
#[test]
fn query_request() {
let mut request = RequestSpec {
method: Method("GET".to_string()),
url: Url::from_str("http://localhost:8000/querystring-params").unwrap(),
querystring: vec![
Param {
name: String::from("param1"),
value: String::from("value1"),
},
Param {
name: String::from("param2"),
value: String::from("a b"),
},
],
..Default::default()
};
let context_dir = ContextDir::default();
let cookie_store = CookieStore::new();
let options = ClientOptions::default();
let output = None;
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl 'http://localhost:8000/querystring-params?param1=value1¶m2=a%20b'",
);
request.url =
Url::from_str("http://localhost:8000/querystring-params?param3=foo¶m4=bar")
.unwrap();
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl 'http://localhost:8000/querystring-params?param3=foo¶m4=bar¶m1=value1¶m2=a%20b'",
);
}
#[test]
fn form_request() {
let mut headers = HeaderVec::new();
headers.push(Header::new(
"Content-Type",
"application/x-www-form-urlencoded",
));
let request = RequestSpec {
method: Method("POST".to_string()),
url: Url::from_str("http://localhost/form-params").unwrap(),
headers,
form: vec![Param::new("param1", "value1"), Param::new("param2", "a b")],
implicit_content_type: Some("multipart/form-data".to_string()),
..Default::default()
};
let context_dir = ContextDir::default();
let cookie_store = CookieStore::new();
let options = ClientOptions::default();
let output = None;
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data 'param1=value1' \
--data 'param2=a%20b' \
'http://localhost/form-params'"
);
}
#[test]
fn json_request() {
let mut headers = HeaderVec::new();
headers.push(Header::new("content-type", "application/vnd.api+json"));
let mut request = RequestSpec {
method: Method("POST".to_string()),
url: Url::from_str("http://localhost/json").unwrap(),
headers,
body: Body::Text(String::new()),
implicit_content_type: Some("application/json".to_string()),
..Default::default()
};
let context_dir = ContextDir::default();
let cookie_store = CookieStore::new();
let options = ClientOptions::default();
let output = None;
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl \
--request POST \
--header 'content-type: application/vnd.api+json' \
'http://localhost/json'"
);
request.body = Body::Text("{\"foo\":\"bar\"}".to_string());
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl \
--header 'content-type: application/vnd.api+json' \
--data '{\"foo\":\"bar\"}' \
'http://localhost/json'"
);
request.method = Method("PUT".to_string());
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl \
--request PUT \
--header 'content-type: application/vnd.api+json' \
--data '{\"foo\":\"bar\"}' \
'http://localhost/json'"
);
}
#[test]
fn post_binary_file() {
let request = RequestSpec {
method: Method("POST".to_string()),
url: Url::from_str("http://localhost:8000/hello").unwrap(),
body: Body::File(b"Hello World!".to_vec(), "foo.bin".to_string()),
..Default::default()
};
let context_dir = ContextDir::default();
let cookie_store = CookieStore::new();
let options = ClientOptions::default();
let output = None;
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl \
--header 'Content-Type:' \
--data-binary '@foo.bin' \
'http://localhost:8000/hello'"
);
}
#[test]
fn test_encode_byte() {
assert_eq!(encode_byte(1), "\\x01".to_string());
assert_eq!(encode_byte(32), "\\x20".to_string());
}
#[test]
fn header_curl_args() {
assert_eq!(
Header::new("Host", "example.com").curl_args(),
vec!["--header".to_string(), "'Host: example.com'".to_string()]
);
assert_eq!(
Header::new("If-Match", "\"e0023aa4e\"").curl_args(),
vec![
"--header".to_string(),
"'If-Match: \"e0023aa4e\"'".to_string()
]
);
}
#[test]
fn param_curl_args() {
assert_eq!(
Param {
name: "param1".to_string(),
value: "value1".to_string(),
}
.curl_arg(),
"param1=value1".to_string()
);
assert_eq!(
Param {
name: "param2".to_string(),
value: String::new(),
}
.curl_arg(),
"param2=".to_string()
);
assert_eq!(
Param {
name: "param3".to_string(),
value: "a=b".to_string(),
}
.curl_arg_escape(),
"param3=a%3Db".to_string()
);
assert_eq!(
Param {
name: "param4".to_string(),
value: "1,2,3".to_string(),
}
.curl_arg_escape(),
"param4=1%2C2%2C3".to_string()
);
}
#[test]
fn test_encode_body() {
let current_dir = Path::new("/tmp");
let file_root = Path::new("/tmp");
let context_dir = ContextDir::new(current_dir, file_root);
assert_eq!(
Body::Text("hello".to_string()).curl_arg(&context_dir),
"'hello'".to_string()
);
if cfg!(unix) {
assert_eq!(
Body::File(vec![], "filename".to_string()).curl_arg(&context_dir),
"'@/tmp/filename'".to_string()
);
}
assert_eq!(
Body::Binary(vec![1, 2, 3]).curl_arg(&context_dir),
"$'\\x01\\x02\\x03'".to_string()
);
}
#[test]
fn test_encode_shell_string() {
assert_eq!(encode_shell_string("hello"), "'hello'");
assert_eq!(encode_shell_string("\\n"), "'\\n'");
assert_eq!(encode_shell_string("'"), "$'\\''");
assert_eq!(encode_shell_string("\\'"), "$'\\\\\\''");
assert_eq!(encode_shell_string("\n"), "$'\\n'");
}
#[test]
fn test_escape_string() {
assert_eq!(escape_string("hello"), "hello");
assert_eq!(escape_string("\\n"), "\\\\n");
assert_eq!(escape_string("'"), "\\'");
assert_eq!(escape_string("\\'"), "\\\\\\'");
assert_eq!(escape_string("\n"), "\\n");
}
#[test]
fn test_escape_mode() {
assert!(!escape_mode("hello"));
assert!(!escape_mode("\\"));
assert!(escape_mode("'"));
assert!(escape_mode("\n"));
}
#[test]
fn cookie_with_single_quote() {
let request = RequestSpec {
method: Method("GET".to_string()),
url: Url::from_str("http://localhost:8000/hello").unwrap(),
cookies: vec![crate::http::RequestCookie {
name: "cookie1".to_string(),
value: "value'with'quotes".to_string(),
}],
..Default::default()
};
let context_dir = ContextDir::default();
let cookie_store = CookieStore::new();
let options = ClientOptions::default();
let output = None;
let cmd = CurlCmd::new(
&request,
&cookie_store,
&context_dir,
output.as_ref(),
&options,
);
assert_eq!(
cmd.to_string(),
"curl --cookie $'cookie1=value\\'with\\'quotes' 'http://localhost:8000/hello'"
);
}
}