Python equivalent of Ruby's Array#pack, how to pack unknown string length and bytes together

huangapple go评论63阅读模式
英文:

Python equivalent of Ruby's Array#pack, how to pack unknown string length and bytes together

问题

I understand that you want assistance with translating the provided text, specifically the code sections, without addressing the content or questions. Here's the translated code sections:

def to_s
    entries = @entries.sort_by(&:name).map do |entry|
      "#{MODE} #{entry.name}".encode(Encoding::ASCII) + [entry.oid].pack(ENTRY_FORMAT)
    end
    entries.join
end
def to_s(self):
    entries = sorted(self.entries, key=lambda x: x.name)
    entries = [f"{self.MODE} {entry.name}".encode() + entry.oid for entry in entries]
    packed_entries = b"".join(pack("!s40s", entry) for entry in entries)
    return packed_entries

Please note that I have translated the code sections, and they should now be in Chinese. If you have any further translation needs or questions, feel free to ask.

英文:

I am working my way through the book "Building Git", which goes through building Git with Ruby. I decided to write it in python while still following along in the book.

The author uses a function defined in ruby Array#pack to pack a git tree object. Git uses binary representation for the 40 character blob hash to reduce it to 20 bytes. In the authors words:

> Putting everything together, this generates a string for each entry consisting of the mode 100644,
a space, the filename, a null byte, and then twenty bytes for the object ID. Ruby’s Array#pack
supports many more data encodings and is very useful for generating binary representations of
values. If you wanted to, you could implement all the maths for reading pairs of digits from
the object ID and turning each pair into a single byte, but Array#pack is so convenient that I
usually reach for that first.

He uses the following code to implement this:

def to_s
    entries = @entries.sort_by(&:name).map do |entry|
      ["#{ MODE } #{ entry.name }", entry.oid].pack(ENTRY_FORMAT)
    end

with ENTRY_FORMAT = "Z*H40" and MODE = "100644".
entry is class that has :name and :oid attributes, representing the name and the SHA1 hash of a filename.

The goal is also explained by the author:
> Putting everything together, this generates a string for each entry consisting of the mode 100644,
a space, the filename, a null byte, and then twenty bytes for the object ID. Ruby’s Array#pack
supports many more data encodings and is very useful for generating binary representations of
values. If you wanted to, you could implement all the maths for reading pairs of digits from
the object ID and turning each pair into a single byte, but Array#pack is so convenient that I
usually reach for that first.

And the format "Z*H40" means the following:

> Our usage here consists of two separate encoding instructions:
> - Z*: this encodes the first string, "#{ MODE } #{ entry.name }", as an arbitrary-length null-
padded string, that is, it represents the string as-is with a null byte appended to the end
> - H40: this encodes a string of forty hexadecimal digits, entry.oid, by packing each pair of
digits into a single byte as we saw in Section 2.3.3, “Trees on disk”

I have tried for many hours to replicate this in python using struct.pack and other various methods, but either i am not getting the format correct, or I am just missing something very obvious. In any case, this is what I currently have:

def to_s(self):
      entries = sorted(self.entries, key=lambda x: x.name)

      entries = [f"{self.MODE} {entry.name}" + entry.oid.encode() for entry in entries]
      packed_entries = b"".join(pack("!Z*40s", entry) for entry in entries)

      return packed_entries

but obviously this will give a concat error from bytes() to str().

Traceback (most recent call last):
  File "jit.py", line 67, in <module>
    database.store(tree)
  File "/home/maslin/jit/pyJit/database.py", line 12, in store
    string = obj.to_s()
  File "/home/maslin/jit/pyJit/tree.py", line 40, in to_s
    entries = [f"{self.MODE} {entry.name}" + entry.oid.encode() for entry in entries]
  File "/home/maslin/jit/pyJit/tree.py", line 40, in <listcomp>
    entries = [f"{self.MODE} {entry.name}" + entry.oid.encode() for entry in entries]
TypeError: can only concatenate str (not "bytes") to str

So then I tried to keep everything as a string, and tried using struct.pack to format it for me, but it gave me a struct.error: bad char in struct format error.

def to_s(self):
      entries = sorted(self.entries, key=lambda x: x.name)

      entries = [f"{self.MODE} {entry.name}" + entry.oid for entry in entries]
      packed_entries = b"".join(pack("!Z*40s", entry) for entry in entries)

      return packed_entries

And the traceback:

Traceback (most recent call last):
  File "jit.py", line 67, in <module>
    database.store(tree)
  File "/home/maslin/jit/pyJit/database.py", line 12, in store
    string = obj.to_s()
  File "/home/maslin/jit/pyJit/tree.py", line 41, in to_s
    packed_entries = b"".join(pack("!Z*40s", entry) for entry in entries)
  File "/home/maslin/jit/pyJit/tree.py", line 41, in <genexpr>
    packed_entries = b"".join(pack("!Z*40s", entry) for entry in entries)
struct.error: bad char in struct format

How can I pack a string for each entry consisting of the mode 100644,
a space, the filename, a null byte, and then twenty bytes for the object ID?

The author notes above that this can be done by "implementing all the maths for reading pairs of digits from
the object ID and turning each pair into a single byte", so if your solution involves this method, that is also ok.

P.S. this question did not help me nor did this.

P.P.S. ChatGPT was no help as well

答案1

得分: 1

So, I had to look this up. The binary format is simple,

  • the mode as an ascii byte string,
  • an ascii space
  • the filename as a byte string,
  • a null byte
  • the sha digest in binary format.

So,

mode = b"100644"

Note, mode is a bytes object. You should probably just have it as a bytes object, but if it is a string, you can just .encode it and it should work with utf-8 since it will only be in the ascii range.

Now, your filename is probably a string, e.g.:

filename = "foo.py"

Now, you didn't say exactly, but I presume your oid is the sha1 hexdigest, i.e. a length 40 string of the digest in hexadecimal. However, you probably should just work with the raw digest. Assuming you consumed

>>> import hashlib
>>> sha = hashlib.sha1(b"print('hello, world')")
>>> sha.hexdigest()
'da8b53bb595a2bd0161f6470a4c3a82f6aa1dc9e'
>>> sha.digest()
b'\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'

You want just the .digest() directly. You should probably just keep around the hash object and get whatever you need from there, or you can convert back and forth, so if you have the hexdigest, you can get to the binary using:

>>> oid = sha.hexdigest()
>>> oid
'da8b53bb595a2bd0161f6470a4c3a82f6aa1dc9e'
>>> int(oid, 16).to_bytes(20)
b'\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'

But really, if you are just going to keep one around, I'd keep the binary form, it seems more natural to me to convert to an int then format that in hex:

>>> oid = sha.digest()
>>> oid
b'\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'
>>> int.from_bytes(oid)
1247667085693497210187506196029418989550863244446
>>> f"{int.from_bytes(oid):x}"
'da8b53bb595a2bd0161f6470a4c3a82f6aa1dc9e'

So, I'm going to assume you have:

>>> import hashlib
>>> mode = b"100644"
>>> filename = "foo.py"
>>> sha = hashlib.sha1(b"print('hello, world')")
>>> oid = sha.digest()

Now, there is no f-string-like interpolation for bytes-literals, but you can use the old-school % based formatting:

>>> entry = b"%s %s\x00%s" % (mode, filename.encode(), oid)
>>> entry
b'100644 foo.py\x00\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'

Or since this is so simple, just concatenation:

>>> entry = mode + b" " + filename.encode() + b"\x00" + oid
>>> entry
b'100644 foo.py\x00\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'

Now, you could use struct.pack here, but it's a bit unwieldy. There's no good way to add a space except as a single character. Also, you'd have to dynamically come up with the format string, since there is no format for "arbitrary sized, null-terminated byte string". But you can use an f-string and len(file.encode()) + 1. So it would need to be something like:

>>> struct.pack(f">6sc{len(filename.encode())+1}s20s", mode, b" ", filename.encode(), oid)
b'100644 foo.py\x00\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'
>>> struct.pack(f">6sc{len(filename.encode())+1}s20s", mode, b" ", filename.encode(), oid) == entry
True
英文:

So, I had to look this up. The binary format is simple,

  • the mode as an ascii byte string,
  • an ascii space
  • the filename as a byte string,
  • a null byte
  • the sha digest in binary format.

So,

mode = b"100644"

Note, mode is a bytes object. You should probably just have it as a bytes object,but if it is a string, you can just .encode it and it should work with utf-8 since it will only be in the ascii range.

Now, your filename is probably a string, e.g.:

filename = "foo.py"

Now, you didn't say exactly, but I presume your oid is the sha1 hexdigest, i.e. a length 40 string of the digest in hexadecimal. However, you probably should just work with the raw digest. Assuming you consumed

>>> import hashlib
>>> sha = hashlib.sha1(b"print('hello, world')")
>>> sha.hexdigest()
'da8b53bb595a2bd0161f6470a4c3a82f6aa1dc9e'
>>> sha.digest()
b'\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'

You want just the .digest() directly. You should probably just keep around the hash object and get whatever you need from there, or you can convert back and for, so if you have the hexdigest, you can get to the binary using:

>>> oid = sha.hexdigest()
>>> oid
'da8b53bb595a2bd0161f6470a4c3a82f6aa1dc9e'
    >>> int(oid, 16).to_bytes(20)
b'\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'

Bute really, if you are just going to keep one around, I'd keep the binary form, it seems more natural to me to convert to an int then format that in hex:

>>> oid = sha.digest()
>>> oid
b'\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'
>>> int.from_bytes(oid)
1247667085693497210187506196029418989550863244446
>>> f"{int.from_bytes(oid):x}"
'da8b53bb595a2bd0161f6470a4c3a82f6aa1dc9e'

So, I'm going to assume you have:

>>> import hashlib
>>> mode = b"100644"
>>> filename = "foo.py"
>>> sha = hashlib.sha1(b"print('hello, world')")
>>> oid = sha.digest()

Now, there is no f-string-like interpolation for bytes-literals, but you can use the old-school % based formatting:

>>> entry = b"%s %s\x00%s" % (mode, filename.encode(), oid)
>>> entry
b'100644 foo.py\x00\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'

Or since this is so simple, just concatenation:

>>> entry = mode + b" " + filename.encode() + b"\x00" + oid
>>> entry
b'100644 foo.py\x00\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'

Now, you could use struct.pack here, but it's a bit unwieldy. There's no good way to add a space except as a single characer. Also, you'd have to dynamically come up with the format string, since there is no format for "arbitrary sized, null terminated bytes string". But you can use an f-string and len(file.encode()) + 1. So it would need to be something like:

>>> struct.pack(f">6sc{len(filename.encode())+1}s20s", mode, b" ", filename.encode(), oid)
b'100644 foo.py\x00\xda\x8bS\xbbYZ+\xd0\x16\x1fdp\xa4\xc3\xa8/j\xa1\xdc\x9e'
>>> struct.pack(f">6sc{len(filename.encode())+1}s20s", mode, b" ", filename.encode(), oid) == entry
True

huangapple
  • 本文由 发表于 2023年2月10日 07:07:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/75405392.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定