如何允许 bash 高效读取包含 NUL 字节的数据?

问题描述

更准确地说,问题是:

哪些方法可以使 bash 脚本正确、安全地处理可能包含 NNUL 字节?

这个问题导致了以下观察:

bash -c 'LC_ALL=C read -rN 1 </dev/zero'
  • 使用 Debian 10 的 bash 版本 5.0.17(1)-release 进行测试

(I tried to find out myself but found no pointer why this happens)。到目前为止我发现的是,“我的”bash 显然跳过了 NUL 上的所有 read -N 字节。

-N 1 的特殊情况下可能的解决方法是使用

LC_ALL=C IFS= read -rd '' -n 1

这样 NUL 作为分隔符,所以 read 返回。但是,如果您想跳过超过 1 个字节,这个技巧就会失败,因为 read 在看到第一个 NUL 后终止。

对于特殊情况,有一些变通方法,例如分叉 dd,但是如果您想处理 bash 中的数据或需要经常跳过几个字节,分叉的弊大于利。

如果您想跳过更大的 read -d '' -n 1 区域,循环 NUL 也很麻烦,因为这是每字节一个系统调用

注意事项:

  • 这不是关于哪种解决方案最好的意见问题。
  • 这是一个列出处理最常见情况的方法的问题。
  • 答案应该适用于以下用例:
    • 管道,你找不到的地方
    • 套接字(如 <>"/dev/tcp/$HOST/$PORT"

请始终牢记,“性能”不仅仅包括原始速度。它通常包括您需要更改某些内容的时间,从头开始重写内容需要很长时间,或者插入诸如 dd 之类的东西变得极其困难。很多时候你所拥有的只是纯粹的bash。还有一些帮手。

例如,可能有一些更大的脚本应用于 git fast-export 之类的内容。这个脚本可以完美运行,直到第一个带有 NUL 字节的二进制文件添加到 repo 中。突然 read -N 不同步了,以至于 git fast-import 抱怨。如果代码主要用于编辑提交消息(它们被视为二进制数据),您必须复制代码一个用于二进制,NUL 感知,一个用于提交,以在 bash 中更改。

可能没有一刀切的事情,所以我们可能需要更多的解决方案,而不仅仅是调用 dd

解决方法

bash 正在与管道通话的情况下,以下为我解决了这个问题。

我没有使用 producer | bashscript | consumer,而是将一些转换脚本放入管道中:

producer | encoder | bashscript | decoder | consumer
  • encoder00 转义为 01 02,将 01 转义为 01 03
  • decoder0001 020101 03 进行转义。

然后,在 bash 中,我可以使用以下例程来读取 N 字节:

: readbytes N variable
readbytes()
{
local -n ___var="${2:-REPLY}"
local ___esc ___tmp
LC_ALL=C read -rN "$1" ___var || return     # short read
___esc="$___var"
while   ___esc="${___esc//[^$'\x01']/}"
        ___tmp="${#___esc}"
        [ 0 -lt "$___tmp" ]
do
        ___esc=
        LC_ALL=C read -rN "$___tmp" ___esc
        ___tmp=$?
        ___var="$___var$___esc"
        [ 0 = $___tmp ] || return $___tmp   # short read
done
return 0
}

这个例程有什么作用?

  • 调用 readbytes N variable 首先将 N 个字节读入 variable
  • 然后计算 01 字节 (\1)
  • 每个 01 字节都有第二个字节,因此我们的给定计数很短。
  • 因此请阅读此额外计数并将其附加到 variable
  • 现在,可能还会出现额外的 01 字节,因此我们也需要重新读取它们。
  • 这个循环因此最多在 ld N 步结束
  • 因此,与带有 O(ld N)O(N) 相比,此例程最多只有 read -n 个系统调用。当 00- 和 01-bytes 不存在时,此例程仅执行 1 次系统调用。
  • 整体悲观运行时复杂度有点像 O(N ld N),虽然不完美,但在使用 O(N*N) 时比 read -n 好得多

注意事项:

  • 此例程不解码数据。因此,如果您读取 10 个字节并且其中有一个 NUL,您将返回一个 11 个字节的字符串(NUL 被来自编码器的字节序列 01 02 替换)。

  • 解码器并不总是需要的,因为 bash 非常适合用 NULprintf '\0' 之类的东西写入 printf %b '\0' 字节。但是,如果您主要将 STDIN 复制到 STDOUT 并同时更改一些内容,那么大多数情况下,不转换 bash 内的数据并将其留给解码器会更方便。

  • 可能没有好的方法来解码 bash 中的数据,因为 bash 变量(与所有环境变量一样)不能包含 NUL

这是一个encoder in Python3

#!/usr/bin/env python3

import sys
while 1:
    a = sys.stdin.buffer.read(102400);
    if not a: break
    sys.stdout.buffer.write(a.replace(b'\1',b'\1\3').replace(b'\0',b'\1\2'))

还有 decoder in Python3 只是稍微复杂一点:

#!/usr/bin/env python3

import sys
dang = False
while 1:
    a   = sys.stdin.buffer.read(102400);
    if not a: break
    if dang:
        a   = b'\1'+a
        dang    = False
    if a[-1] == 1:
        dang    = True
        a   = a[:-1]
    sys.stdout.buffer.write(a.replace(b'\1\2',b'\0').replace(b'\1\3',b'\1'))

complete git repo on GitHub 还包含一个 C 代码包装器 bashnul,其运行速度比 Python 代码快得多(C 程序也会检测编码错误等)。

(请注意,它没有经过彻底测试。)

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...