diff --git a/src/runner/core.rs b/src/runner/core.rs index 58625d163..2c4cac84a 100644 --- a/src/runner/core.rs +++ b/src/runner/core.rs @@ -105,6 +105,7 @@ pub enum RunnerError { //??CaptureError {}, InvalidUtf8, InvalidDecoding { charset: String }, + InvalidCharset { charset: String }, // Query QueryHeaderNotFound, @@ -160,10 +161,12 @@ impl FormatError for Error { RunnerError::QueryInvalidJsonpathExpression { .. } => "Invalid jsonpath".to_string(), RunnerError::PredicateType { .. } => "Assert - Inconsistent predicate type".to_string(), RunnerError::SubqueryInvalidInput { .. } => "Subquery error".to_string(), - RunnerError::InvalidDecoding { .. } => "Invalid Decoding".to_string(), + RunnerError::InvalidDecoding { ..} => "Invalid Decoding".to_string(), + RunnerError::InvalidCharset { ..} => "Invalid Charset".to_string(), RunnerError::AssertFailure { .. } => "Assert Failure".to_string(), RunnerError::UnrenderableVariable { .. } => "Unrenderable Variable".to_string(), RunnerError::NoQueryResult { .. } => "No query result".to_string(), + } } @@ -189,6 +192,7 @@ impl FormatError for Error { RunnerError::PredicateType { .. } => "predicate type inconsistent with value return by query".to_string(), RunnerError::SubqueryInvalidInput => "Type from query result and subquery do not match".to_string(), RunnerError::InvalidDecoding { charset } => format!("The body can not be decoded with charset '{}'", charset), + RunnerError::InvalidCharset { charset } => format!("The charset '{}' is not valid", charset), RunnerError::AssertFailure { actual, expected, .. } => format!("actual: {}\nexpected: {}", actual, expected), RunnerError::VariableNotDefined { name } => format!("You must set the variable {}", name), RunnerError::UnrenderableVariable { value } => format!("value {} can not be rendered", value), diff --git a/src/runner/http_response.rs b/src/runner/http_response.rs new file mode 100644 index 000000000..f2d57cf92 --- /dev/null +++ b/src/runner/http_response.rs @@ -0,0 +1,182 @@ +/* + * hurl (https://hurl.dev) + * Copyright (C) 2020 Orange + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +use crate::http::libcurl::core::Response; +use super::core::RunnerError; +use encoding::{EncodingRef, DecoderTrap}; + + +/// +/// get body content as text from http response +/// used by query +/// + +#[allow(dead_code)] +impl Response { + + + /// + /// Return optional Content-type header value + /// + fn content_type(&self) -> Option { + for header in self.headers.clone() { + if header.name.to_lowercase() == "content-type" { + return Some(header.value); + } + } + None + } + + + /// + /// Return encoding of the response + /// + fn encoding(&self) -> Result { + match self.content_type() { + Some(content_type) => { + match mime_charset(content_type) { + Some(charset) => match encoding::label::encoding_from_whatwg_label(charset.as_str()) { + None => Err(RunnerError::InvalidCharset { charset }), + Some(enc) => Ok(enc) + }, + None => Ok(encoding::all::UTF_8), + } + } + None => Ok(encoding::all::UTF_8) + } + } + + pub fn text(&self) -> Result { + let encoding = self.encoding()?; + match encoding.decode(&self.body, DecoderTrap::Strict) { + Ok(s) => Ok(s), + Err(_) => Err(RunnerError::InvalidDecoding { charset: encoding.name().to_string() }) + } + } +} + +/// +/// Extract charset from mime-type String +/// +fn mime_charset(mime_type: String) -> Option { + match mime_type.find("charset=") { + None => None, + Some(index) => { + Some(mime_type[(index + 8)..].to_string()) + } + } +} + + +#[cfg(test)] +pub mod tests { + use super::*; + use crate::http::libcurl::core::{Version, Header}; + + #[test] + pub fn test_charset() { + assert_eq!(mime_charset("text/plain; charset=utf-8".to_string()), Some("utf-8".to_string())); + assert_eq!(mime_charset("text/plain; charset=ISO-8859-1".to_string()), Some("ISO-8859-1".to_string())); + assert_eq!(mime_charset("text/plain;".to_string()), None); + } + + + fn hello_response() -> Response { + Response { + version: Version::Http10, + status: 200, + headers: vec![], + body: b"Hello World!".to_vec(), + } + } + + fn utf8_encoding_response() -> Response { + Response { + version: Version::Http10, + status: 200, + headers: vec![ + Header { name: "Content-Type".to_string(), value: "text/plain; charset=utf-8".to_string() } + ], + body: vec![0x63, 0x61, 0x66, 0xc3, 0xa9], + } + } + + fn latin1_encoding_response() -> Response { + Response { + version: Version::Http10, + status: 200, + headers: vec![ + Header { name: "Content-Type".to_string(), value: "text/plain; charset=ISO-8859-1".to_string() } + ], + body: vec![0x63, 0x61, 0x66, 0xe9], + } + } + + #[test] + pub fn test_content_type() { + assert_eq!(hello_response().content_type(), None); + assert_eq!(utf8_encoding_response().content_type(), Some("text/plain; charset=utf-8".to_string())); + assert_eq!(latin1_encoding_response().content_type(), Some("text/plain; charset=ISO-8859-1".to_string())); + } + + #[test] + pub fn test_encoding() { + assert_eq!(hello_response().encoding().unwrap().name(), "utf-8"); + assert_eq!(utf8_encoding_response().encoding().unwrap().name(), "utf-8"); + assert_eq!(latin1_encoding_response().encoding().unwrap().name(), "windows-1252"); + } + + #[test] + pub fn test_text() { + assert_eq!(hello_response().text().unwrap(), "Hello World!".to_string()); + assert_eq!(utf8_encoding_response().text().unwrap(), "café".to_string()); + assert_eq!(latin1_encoding_response().text().unwrap(), "café".to_string()); + } + + #[test] + pub fn test_invalid_charset() { + assert_eq!(Response { + version: Version::Http10, + status: 200, + headers: vec![ + Header { name: "Content-Type".to_string(), value:"test/plain; charset=xxx".to_string()} + ], + body: b"Hello World!".to_vec(), + }.encoding().err().unwrap(), RunnerError::InvalidCharset { charset: "xxx".to_string()}); + } + + #[test] + pub fn test_invalid_decoding() { + assert_eq!(Response { + version: Version::Http10, + status: 200, + headers: vec![], + body: vec![0x63, 0x61, 0x66, 0xe9], + }.text().err().unwrap(), RunnerError::InvalidDecoding { charset: "utf-8".to_string()}); + + assert_eq!(Response { + version: Version::Http10, + status: 200, + headers: vec![ + Header { name: "Content-Type".to_string(), value: "text/plain; charset=ISO-8859-1".to_string() } + ], + body: vec![0x63, 0x61, 0x66, 0xc3, 0xa9], + }.text().unwrap(), "café".to_string()); + } + +} \ No newline at end of file diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 9ccd43412..0c650ad95 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -30,6 +30,7 @@ mod cookie; pub mod core; mod entry; pub mod file; +mod http_response; mod json; pub mod log; mod predicate;