Prevent raw binary response to be displayed on standard ouput

This commit is contained in:
jcamiel 2024-04-02 16:43:48 +02:00 committed by hurl-bot
parent c8c3cf425f
commit e727e0b826
No known key found for this signature in database
GPG Key ID: 1283A2B4A0DCAF8D
3 changed files with 74 additions and 10 deletions

View File

@ -15,12 +15,16 @@
* limitations under the License.
*
*/
use std::cmp::min;
use std::io::IsTerminal;
use colored::Colorize;
use hurl_core::ast::{Pos, SourceInfo};
use crate::output::Error;
use crate::runner;
use crate::runner::{HurlResult, Output};
use crate::util::term::Stdout;
use crate::util::term::{Stderr, Stdout};
/// Writes the `hurl_result` last response to the file `filename_out`.
///
@ -32,6 +36,7 @@ pub fn write_last_body(
color: bool,
filename_out: Option<&Output>,
stdout: &mut Stdout,
stderr: &mut Stderr,
) -> Result<(), Error> {
// Get the last call of the Hurl result.
let Some(last_entry) = &hurl_result.entries.last() else {
@ -67,22 +72,56 @@ pub fn write_last_body(
let bytes = &response.body;
output.extend(bytes);
}
// We replicate curl's checks for binary output: a warning is displayed when user hasn't
// use `--output` option and the response is considered as a binary content. If user has used
// `--output` whether to save to a file, or to redirect output to standard output (`--output -`)
// we don't display any warning.
match filename_out {
None => {
if std::io::stdout().is_terminal() && is_binary(&output) {
let message = "Binary output can mess up your terminal. Use \"--output -\" to tell Hurl to output it to your terminal anyway, or consider \"--output\" to save to a file.";
let message = if color {
format!("{}: {}", "warning".yellow().bold(), message.bold())
} else {
format!("warning: {message}")
};
stderr.eprintln(&message);
// We don't want to have any additional error message.
return Err(Error::new(""));
}
Output::Stdout.write(&output, stdout, None)?;
}
Some(out) => out.write(&output, stdout, None)?,
None => Output::Stdout.write(&output, stdout, None)?,
}
Ok(())
}
/// Returns `true` if `bytes` is a binary content, false otherwise.
///
/// For the implementation, we use a simple heuristic on the buffer: just check the presence of NULL
/// in the first 2000 bytes to determine if the content if binary or not.
///
/// See <https://github.com/curl/curl/pull/1512>
/// and <https://github.com/curl/curl/blob/721941aadf4adf4f6aeb3f4c0ab489bb89610c36/src/tool_cb_wrt.c#L209>
fn is_binary(bytes: &[u8]) -> bool {
let len = min(2000, bytes.len());
for c in &bytes[..len] {
if *c == 0 {
return true;
}
}
false
}
#[cfg(test)]
mod tests {
use crate::http::{Call, Header, HeaderVec, HttpVersion, Request, Response};
use crate::output::write_last_body;
use crate::runner::{EntryResult, HurlResult, Output};
use crate::util::term::{Stdout, WriteMode};
use crate::util::term::{Stderr, Stdout, WriteMode};
use hurl_core::ast::{Pos, SourceInfo};
fn hurl_result() -> HurlResult {
fn hurl_result_json() -> HurlResult {
let mut headers = HeaderVec::new();
headers.push(Header::new("x-foo", "xxx"));
headers.push(Header::new("x-bar", "yyy0"));
@ -167,13 +206,22 @@ mod tests {
#[test]
fn write_last_body_with_headers() {
let result = hurl_result();
let result = hurl_result_json();
let include_header = true;
let color = false;
let output = Some(Output::Stdout);
let mut stdout = Stdout::new(WriteMode::Buffered);
let mut stderr = Stderr::new(WriteMode::Buffered);
write_last_body(&result, include_header, color, output.as_ref(), &mut stdout).unwrap();
write_last_body(
&result,
include_header,
color,
output.as_ref(),
&mut stdout,
&mut stderr,
)
.unwrap();
let stdout = String::from_utf8(stdout.buffer().to_vec()).unwrap();
assert_eq!(
stdout,

View File

@ -204,7 +204,7 @@ impl ParallelRunner {
}
// Then, we print job output on standard output.
self.print_output(&msg.result, &mut stdout)?;
self.print_output(&msg.result, &mut stdout, &mut stderr)?;
// Report the completion of this job and update the progress.
self.progress.print_completed(&msg.result, &mut stderr);
@ -245,7 +245,12 @@ impl ParallelRunner {
/// Prints a job `result` to standard output `stdout`, either as a raw HTTP response (last
/// body of the run), or in a structured JSON way.
fn print_output(&self, result: &JobResult, stdout: &mut Stdout) -> Result<(), JobError> {
fn print_output(
&self,
result: &JobResult,
stdout: &mut Stdout,
stderr: &mut Stderr,
) -> Result<(), JobError> {
let job = &result.job;
let content = &result.content;
let hurl_result = &result.hurl_result;
@ -264,6 +269,7 @@ impl ParallelRunner {
color,
filename_out,
stdout,
stderr,
);
if let Err(e) = result {
return Err(JobError::Runtime(e.to_string()));

View File

@ -25,7 +25,7 @@ use crate::{cli, HurlRun};
use hurl::parallel::job::{Job, JobResult};
use hurl::parallel::runner::ParallelRunner;
use hurl::runner::{HurlResult, Input};
use hurl::util::term::{Stdout, WriteMode};
use hurl::util::term::{Stderr, Stdout, WriteMode};
use hurl::{output, parallel, runner};
/// Runs Hurl `files` sequentially, given a current directory and command-line options (see
@ -64,7 +64,15 @@ pub fn run_seq(
// representation of the full Hurl result.
// In sequential run, we use an immediate (non-buffered) standard output.
let mut stdout = Stdout::new(WriteMode::Immediate);
print_output(&hurl_result, &content, filename, options, &mut stdout)?;
let mut stderr = Stderr::new(WriteMode::Immediate);
print_output(
&hurl_result,
&content,
filename,
options,
&mut stdout,
&mut stderr,
)?;
let run = HurlRun {
content,
@ -87,6 +95,7 @@ fn print_output(
filename: &Input,
options: &CliOptions,
stdout: &mut Stdout,
stderr: &mut Stderr,
) -> Result<(), CliError> {
let output_body = hurl_result.success
&& !options.interactive
@ -98,6 +107,7 @@ fn print_output(
options.color,
options.output.as_ref(),
stdout,
stderr,
);
if let Err(e) = result {
return Err(CliError::Runtime(e.to_string()));