3

After a lot of struggling, I found out some behavior I can't explain / solve, so asking here for help. On our server (Ubuntu 16.04.5 LTS with PHP 7.0.30), we're using some tools outside of the 'httpdocs', which are called using exec() to get their outputs. In this case, it's a QRCode-generator.

Some of the QR-codes however, won't be displayed. We got an output from the tool (outputting data of a PNG), but when we show it as an image, it appears to be broken for some reason.

After a lot of debugging I found out the result sometimes differs 1 or 2 bytes from the output of the tool.

I did my last debugging using the QR-code below (12345), which is a file of 240 bytes. However, when outputting it eventually, the length appears to be 239 bytes, so we lost a byte somewhere?

Example QR-code

I found out using exec(), we're creating an array of the output. Trailing whitespace, such as \n, is not included in this array, so implode() to glue the array back to a string, like below:

<?php

$cmd = 'cat qr.png';
$output = array();
$exit_code = 0;

exec($cmd, $output, $exit_code);

if($exit_code === 0)
{
    header ("Content-type: image/png;");
    print implode(PHP_EOL, $output);
}
die();

But for some reason, a byte is lost in this process? I already tried some other solutions using shell_exec() (but no return_var here, so we can't check validate the proces...)

<?php

$command = 'cat qr.png';

$output = shell_exec($command);
if($output !== null)
{
    header ("Content-type: image/png;");
    print $output;
}
die();

... and using passthru() (but this outputs the contents directly, which isn't desired. Output-buffering like in the example below isn't possible in the actual code...)

<?php

$command = 'cat qr.png';
$return_var = 0;

ob_start();
passthru($command, $return_var);

if($return_var === 0)
{
    header ("Content-type: image/png;");
    $output = ob_get_clean();
    print $output;
}
die();

So far, the exec()-function always worked pretty well for us, resulting in a $return_var AND $output, but now I'm losing bytes. I already tried some variants of the PHP_EOL, we're now using as glue (\n, \r and \n\r), but with the first 2 line-endings, I still only got 239 bytes and with the last one, I end up with 241 bytes, so 1 byte to much.

Why am I losing a byte here? What is the correct way to convert the $output-array back into a string? Is there a way to get the 240-bytes output back by imploding the array? Or are there other functions I haven't found yet to execute a command which will give me the output as well as the return_var?

Pieter van den Ham
  • 4,381
  • 3
  • 26
  • 41
Bazardshoxer
  • 545
  • 1
  • 5
  • 22
  • I strongly suspect something funky related to line endings is going on. Did you compare the broken received file with the correct one? Also, what about [readfile](http://php.net/manual/en/function.readfile.php)? – fvu Jul 30 '18 at 21:26
  • Is there a difference in character encoding? Are the line-endings actually the same? Did you also try to look up any differences in the files? Also; if the images are exactly the same, you might not want to spend too much time debugging this on binary level. – Webber Jul 30 '18 at 21:27
  • inspecting the 2 files with a hex-viewer tool could give you a clue of that missing byte. Is it a line break? at the start? at the end? are the 2 files identical except of this byte? – Accountant م Jul 30 '18 at 21:47
  • In this case, the command is `cat qr.png`, but in the final solution, this is something like `/usr/local/bin/tools/generate-qr --input="12345"`, so readfile won't work. The encoding is the same, but I'm missing a line-end somewhere. When differencing the files, I see a upside down question mark in the working file (on 3rd line), but missing in the broken output. When using ord() from PHP, pasting the upside-down question-mark in a string, it tells me it's value 13 (Enter?).... So it's a newline I'm missing from exec(), but how do I get it back correctly? – Bazardshoxer Jul 30 '18 at 21:47
  • I'm sorry I can't get what is going on here. How do you send your command line output to the client after imploding it with line breaks as `Content-type: image/png;` ? does your tool generate the QR-code as text outputted in the command line in form of symbols like this [▄█▄](https://www.messletters.com/en/text-art/) or generates an image binary file and save it in the file system ? – Accountant م Jul 30 '18 at 21:59
  • @Accountantم The tool for creating the QR-code (outputting a PNG) was formerly placed inside the httpdocs, getting the images with a cUrl-request and embedding them into an HTML e-mail using src='data:...'. But we want to remove load of the server, so instead of using cUrl, we want to create the QR-code directly on the server (no requests/Apache) and using the output from the shell-command to embed the generated PNG. But for some reason, after imploding the output from exec() we seam to loose data. Using a hex-viewer, I found out it was 0D (dec. 13, newline) missing in the broken image/output. – Bazardshoxer Jul 30 '18 at 22:29

1 Answers1

2

According to the manual entry for exec, the function returns the last line of output. Whether that last line is also included as the last element of $output is unclear.

But more importantly the the manual states:

Trailing whitespace, such as \n, is not included in this array.

I would guess that this is per-element of the array (i.e. per-line of your output), but either way, your whitespace will be significant here, because you're not dealing with text - all the bytes are important and meaningful here. Remember, whitespace doesn't just mean \n. It can also mean \t or (a space), for example.

In the event that a line ends with F\t\n (a capital F, or any other non-whitespace character, a tab, then a newline), both whitespace characters would be stripped from the end. When doing your implode operation, you might be putting the \n back, but you'll never know about the \t that was stripped.

The key thing to realise here is that exec is expecting to be dealing with plain text not raw binary data.

I would suggest that instead of using cat qr.png, you use base64 qr.png to encode the binary data into an ASCII string, then decode it in PHP with base64_decode. If you're not going to be using cat in reality, you can still pipe the output of your QR-generating command through base64 like this: generate-qr-png |base64. There shouldn't be any significant whitespace in that situation, so nothing would get munged by exec.

Alex
  • 3,029
  • 3
  • 23
  • 46
  • It is indeed a whitespace problem. I guess a line end with a tab or something is the problem here. I hoped glueing the output from `exec` back together would result into the same result, but there's a difference between, causing broken files. Base64 encoding and decoding it seams to solve the issue, thanks! With `|base64` I get the right output. Because it'll be an embedded image into an e-mail, the base64_decode isn't even necessary (did an encode when putting the impage into src='data:...', but this is now done by the command already :) – Bazardshoxer Jul 30 '18 at 22:34
  • Sounds like it was meant to be :) Please do mark the answer as accepted then if it's solved your problem. Glad I could help! – Alex Jul 31 '18 at 01:10