Python:ctypes hashable c_char数组替换而不会跳过'\ 0'字节

时间:2016-12-10 18:18:41

标签: python python-3.x ctypes binaryfiles mmap

为了便于说明,此脚本创建一个文件viewDidLoad(),其中包含作为参数给出的文件内容,前缀为带有sha1校验和的二进制标头,允许在后续运行中进行重复检测。

这里需要的是一个可清除的mapfile替换,它可以使用最少的模糊来保存sha1校验和,但不会阻塞'\ 0'字节。

ctypes.c_char

运行:# -*- coding: utf8 -* import io import mmap import ctypes import hashlib import logging from collections import OrderedDict log = logging.getLogger(__file__) def align(size, alignment): """return size aligned to alignment""" excess = size % alignment if excess: size = size - excess + alignment return size class Header(ctypes.Structure): Identifier = b'HEAD' _fields_ = [ ('id', ctypes.c_char * 4), ('hlen', ctypes.c_uint16), ('plen', ctypes.c_uint32), ('name', ctypes.c_char * 128), ('sha1', ctypes.c_char * 20), ] HeaderSize = ctypes.sizeof(Header) class CtsMap: def __init__(self, ctcls, mm, offset = 0): self.ctcls = ctcls self.mm = mm self.offset = offset def __enter__(self): mm = self.mm offset = self.offset ctsize = ctypes.sizeof(self.ctcls) if offset + ctsize > mm.size(): newsize = align(offset + ctsize, mmap.PAGESIZE) mm.resize(newsize) self.ctinst = self.ctcls.from_buffer(mm, offset) return self.ctinst def __exit__(self, exc_type, exc_value, exc_traceback): del self.ctinst self.ctinst = None class MapFile: def __init__(self, filename): try: # try to create initial file mapsize = mmap.PAGESIZE self._fd = open(filename, 'x+b') self._fd.write(b'\0' * mapsize) except FileExistsError: # file exists and is writable self._fd = open(filename, 'r+b') self._fd.seek(0, io.SEEK_END) mapsize = self._fd.tell() # mmap this file completely self._fd.seek(0) self._mm = mmap.mmap(self._fd.fileno(), mapsize) self._offset = 0 self._toc = OrderedDict() self.gen_toc() def gen_toc(self): while self._offset < self._mm.size(): with CtsMap(Header, self._mm, self._offset) as hd: if hd.id == Header.Identifier and hd.hlen == HeaderSize: self._toc[hd.sha1] = self._offset log.debug('toc: [%s]%s: %s', len(hd.sha1), hd.sha1, self._offset) self._offset += HeaderSize + hd.plen else: break del hd def add_data(self, datafile, data): datasize = len(data) sha1 = hashlib.sha1() sha1.update(data) digest = sha1.digest() if digest in self._toc: log.debug('add_data: %s added already', digest) return None log.debug('add_data: %s, %s bytes, %s', datafile, datasize, digest) with CtsMap(Header, self._mm, self._offset) as hd: hd.id = Header.Identifier hd.hlen = HeaderSize hd.plen = datasize hd.name = datafile hd.sha1 = digest del hd self._offset += HeaderSize log.debug('add_data: %s', datasize) blktype = ctypes.c_char * datasize with CtsMap(blktype, self._mm, self._offset) as blk: blk.raw = data del blk self._offset += datasize return HeaderSize + datasize def close(self): self._mm.close() self._fd.close() if __name__ == '__main__': import os import sys logconfig = dict( level = logging.DEBUG, format = '%(levelname)5s: %(message)s', ) logging.basicConfig(**logconfig) mf = MapFile('mapfile') for datafile in sys.argv[1:]: if os.path.isfile(datafile): try: data = open(datafile, 'rb').read() except OSError: continue else: mf.add_data(datafile.encode('utf-8'), data) mf.close()

第二次调用它,它使用sha1摘要作为键读取文件,收集有序字典中的所有项目。不幸的是,c_char数组语义有点连线,因为它的行为类似'\ 0'终止的c字符串,导致这里的校验和被截断。

见第3和第4行:

python3 hashable_ctypes_bytes.py somefiles*

通常的建议是用c_byte * 20替换c_char * 20,并以此方式丢失透明字节处理。除了数据转换麻烦之外,c_byte数组由于是字节数组而不可清除。我没有找到一个实际的解决方案,没有经历来回的重度转换麻烦,或采用hexdigest,这使sha1摘要大小要求翻倍。

我认为,c_char设计决定将它与C零终止语义混合起来是一个错误。为了解决这个问题,我可以想象将c_char_nz类型添加到ctypes,这可以解决这个问题。

对于那些仔细阅读代码的人,您可能想知道ctypes结构的del语句。可以在here:找到对它的讨论。

1 个答案:

答案 0 :(得分:0)

虽然以下代码正在执行您提到的来回转换,但它确实可以很好地隐藏问题。我发现一个包含null的哈希,该字段现在可以用作字典键。希望它有所帮助。

from ctypes import *
import hashlib

class Test(Structure):
    _fields_ = [('_sha1',c_ubyte * 20)]

    @property
    def sha1(self):
        return bytes(self._sha1)

    @sha1.setter
    def sha1(self, value):
        self._sha1 = (c_ubyte * 20)(*value)

test = Test()
test.sha1 = hashlib.sha1(b'aaaaaaaaaaa').digest()
D = {test.sha1:0}
print(D)

输出:

{b'u\\\x00\x1fJ\xe3\xc8\x84>ZP\xddj\xa2\xfa#\x89=\xd3\xad': 0}