为什么exec()会默默地丢弃字节?

3
经过一番努力,我发现了一些无法解释/解决的行为,所以在此寻求帮助。在我们的服务器上(Ubuntu 16.04.5 LTS with PHP 7.0.30),我们使用一些位于 'httpdocs' 之外的工具,这些工具使用 exec() 调用以获取它们的输出。在这种情况下,它是一个 QRCode 生成器。

然而,其中一些 QR 码将不会被显示。我们从工具中得到了一个输出(输出 PNG 数据),但当我们将其显示为图像时,由于某种原因它似乎已经损坏了。

经过大量调试,我发现结果有时与工具的输出相差 1 或 2 个字节。

我最后使用下面的 QR 码进行了调试(12345),它是一个 240 字节的文件。然而,当最终输出它时,长度似乎只有 239 字节,所以我们在某个地方丢失了一个字节?

Example QR-code

我发现使用 exec(),我们正在创建一个输出数组。尾部空格(例如 \n)不包括在此数组中,因此可以使用implode()将数组粘合回字符串,如下所示:
<?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();

但是由于某些原因,在这个过程中丢失了一个字节?我已经尝试了一些其他解决方案,使用shell_exec()但是这里没有return_var,所以我们无法检查验证过程...
<?php

$command = 'cat qr.png';

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

...并使用passthru()(但这会直接输出内容,这不是所需的。像下面示例中的输出缓冲区在实际代码中不可行...)

<?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();

到目前为止,exec() 函数一直对我们非常有效,可以得到 $return_var $output,但现在我正在失去字节。我已经尝试了一些 PHP_EOL 的变体,我们现在使用它们作为粘合剂(\n,\r 和 \n\r),但是对于前两个行结束符,我仍然只得到了 239 个字节,而对于最后一个行结束符,我最终得到了 241 个字节,因此多了 1 个字节。
我为什么会丢失一个字节?将 $output 数组转换回字符串的正确方法是什么?是否有一种方法通过将数组合并来获得 240 字节的输出?或者是否有其他函数可以执行命令,同时给出输出和 return_var?

我强烈怀疑与行尾符有关的一些奇怪问题正在发生。您是否将损坏的接收文件与正确的文件进行比较?另外,readfile 怎么样? - fvu
字符编码有区别吗?行尾实际上是相同的吗?你是否也尝试查找文件中的任何差异?此外,如果图像完全相同,您可能不想在二进制级别上花费太多时间调试。 - Webber
使用十六进制查看工具检查这两个文件可能会给你一个缺失字节的线索。它是在开头还是结尾的换行符?这两个文件除了这个字节之外是否完全相同? - Accountant م
在这种情况下,命令是cat qr.png,但在最终解决方案中,它类似于/usr/local/bin/tools/generate-qr --input="12345",因此readfile无法使用。编码相同,但我在某个地方缺少一个换行符。当对比文件时,在工作文件(第3行)中看到了一个倒置的问号,但在损坏的输出中却没有。当使用PHP的ord()函数将倒置的问号粘贴到字符串中时,它告诉我它的值为13(回车?)...所以我从exec()中缺少一个换行符,但如何正确地获取它呢? - Bazardshoxer
抱歉,我不明白这里发生了什么。在使用换行符将其拼接为Content-type: image / png;后,您如何将命令行输出发送到客户端?您的工具是否会生成QR码作为类似于▄█▄这样的符号文本输出,并将其保存为图像二进制文件到文件系统中? - Accountant م
@Accountantم 创建QR码(输出PNG)的工具以前放在httpdocs中,使用cUrl请求获取图像,并使用src ='data:...'将它们嵌入HTML电子邮件中。但我们想要减轻服务器负载,所以我们想直接在服务器上创建QR码(无需请求/Apache),并使用来自shell命令的输出来嵌入生成的PNG。但由于某种原因,在执行exec()后,我们似乎丢失了数据。使用十六进制查看器,我发现损坏的图像/输出中缺少0D(dec.13,换行符)。 - Bazardshoxer
1个回答

2
根据exec的手册条目,该函数返回输出的最后一行。不清楚是否还将该行作为$output的最后一个元素返回。
但更重要的是手册说明:
引用如下:

此数组不包括尾随空格,例如\n。

我猜这是针对数组的每个元素(即您的输出的每一行)(per-element of the array),但无论如何,在这里您的空格将很重要,因为您不处理文本-所有字节在此处都是重要和有意义的。请记住,空格不仅意味着\n。例如,它也可以是\t(一个空格)。
如果一行以F\t\n(大写F或任何其他非空白字符,制表符,然后换行符)结尾,则两个空格字符将从末尾删除。当执行您的implode操作时,您可能会将\n放回,但您将永远不知道被剥离的\t
关键要认识到的是exec期望处理的是纯文本而不是原始二进制数据
我建议您使用base64 qr.png将二进制数据编码为ASCII字符串,然后使用base64_decode在PHP中解码它,而不是使用cat qr.png。如果您实际上不会使用cat,则仍然可以通过generate-qr-png |base64将QR生成命令的输出通过管道传输到base64。在这种情况下,不应该有任何重要的空格,因此exec不会对任何内容进行更改。

这确实是一个空格问题。我猜测以制表符或其他字符结尾的行是问题所在。我希望将exec的输出粘合在一起会得到相同的结果,但是它们之间存在差异,导致文件损坏。Base64编码和解码似乎解决了这个问题,谢谢!使用|base64可以得到正确的输出。因为它将成为嵌入电子邮件中的图像,所以甚至不需要进行base64解码(在将图像放入src='data:...'时进行了编码,但现在该命令已经完成了 :))。 - Bazardshoxer
听起来就像是注定的 :) 如果这个答案解决了你的问题,请标记为已接受。很高兴能帮到你! - Alex

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接