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

如何允许bash高效地读取包含NUL字节的数据?,bash,binary,byte,nul,Bash,Binary,Byte,Nul,更确切地说,问题是: 有哪些方法可以使bash脚本正确、安全地处理N可能包含NUL的字节? 这个问题导致了以下观察结果: bash -c 'LC_ALL=C read -rN 1 </dev/zero' 这样NUL充当分隔符,因此read返回。但是,如果您想跳过超过1个字节,则此技巧将失败,因为读取将在看到第一个NUL后终止 对于特殊情况,有一些变通方法,比如分叉dd,但是如果您想在bash中处理数据,或者需要经常跳过几个字节,那么分叉会带来更大的伤害 如果您想跳过更大的NUL区域,那么

更确切地说,问题是:

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

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

bash -c 'LC_ALL=C read -rN 1 </dev/zero'
这样
NUL
充当分隔符,因此
read
返回。但是,如果您想跳过超过1个字节,则此技巧将失败,因为
读取将在看到第一个
NUL
后终止

对于特殊情况,有一些变通方法,比如分叉
dd
,但是如果您想在
bash
中处理数据,或者需要经常跳过几个字节,那么分叉会带来更大的伤害

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

注:

  • 这不是一个关于哪种解决方案最好的意见的问题
  • 这是一个列出处理最常见案例的方法的问题
  • 答案应适用于以下用例:
    • 管道,你找不到的地方
    • 套接字(如
      “/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
  • 编码器
    00
    转义为
    01 02
    并将
    01
    转义为
    01 03
  • decoder
    unescape
    00
    from
    0102
    01
    from
    0103
然后,在
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
    -字节都有一个第二个字节,因此我们缺少给定的计数
  • 因此,读取这个额外的计数并将其附加到
    变量中
  • 现在,再次显示额外的
    01
    -字节,因此我们也需要重新读取它们
  • 因此,此循环最多以
    ld N
    步结束
  • 因此,与使用
    read-N
    O(N)
    相比,这个例程最多有
    O(ldn)
    系统调用。当缺少
    00
    -和
    01
    -字节时,此例程仅执行1个系统调用
  • 总体悲观的运行时复杂性有点像
    O(nldn)
    ,这并不完美,但在使用
    read-N
注:

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

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

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

以下是一份:

还有一个更复杂的问题:

#!/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'))
还包含一个C代码包装器
bashnul
,它的运行速度比Python代码快得多(C程序还检测编码错误等)

(注意,它没有经过粗略测试。)

#!/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'))
#!/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'))