Make cstruct_test pass on both Python 2 and Python 3.
This is in its own CL because most of the other tests depend on
cstruct working.
Test: python2 cstruct_test.py && python3 cstruct_test.py
Test: existing python2 tests pass on android14-5.15
Change-Id: I1e95d4a645456507e2e14f54456a77916fd4a725
diff --git a/net/test/cstruct.py b/net/test/cstruct.py
index 662f2a1..c10667a 100644
--- a/net/test/cstruct.py
+++ b/net/test/cstruct.py
@@ -67,6 +67,7 @@
>>>
"""
+import binascii
import ctypes
import string
import struct
@@ -104,10 +105,18 @@
def Struct(name, fmt, fieldnames, substructs={}):
"""Function that returns struct classes."""
- class CStruct(object):
- """Class representing a C-like structure."""
+ # Hack to make struct classes use the StructMetaclass class on both python2 and
+ # python3. This is needed because in python2 the metaclass is assigned in the
+ # class definition, but in python3 it's passed into the constructor via
+ # keyword argument. Works by making all structs subclass CStructSuperclass,
+ # whose __new__ method uses StructMetaclass as its metaclass.
+ #
+ # A better option would be to use six.with_metaclass, but the existing python2
+ # VM image doesn't have the six module.
+ CStructSuperclass = type.__new__(StructMetaclass, 'unused', (), {})
- __metaclass__ = StructMetaclass
+ class CStruct(CStructSuperclass):
+ """Class representing a C-like structure."""
# Name of the struct.
_name = name
@@ -132,8 +141,11 @@
laststructindex += 1
_format += "%ds" % len(_nested[index])
elif fmt[i] == "A":
- # Null-terminated ASCII string.
- index = CalcNumElements(fmt[:i])
+ # Null-terminated ASCII string. Remove digits before the A, so we don't
+ # call CalcNumElements on an (invalid) format that ends with a digit.
+ start = i
+ while start > 0 and fmt[start - 1].isdigit(): start -= 1
+ index = CalcNumElements(fmt[:start])
_asciiz.add(index)
_format += "s"
else:
@@ -250,13 +262,22 @@
return struct.pack(self._format, *values)
def __str__(self):
+
+ def HasNonPrintableChar(s):
+ for c in s:
+ # Iterating over bytes yields chars in python2 but ints in python3.
+ if isinstance(c, int): c = chr(c)
+ if c not in string.printable: return True
+ return False
+
def FieldDesc(index, name, value):
- if isinstance(value, bytes):
+ if isinstance(value, bytes) or isinstance(value, str):
if index in self._asciiz:
- value = value.rstrip(b"\x00")
- elif any(c not in string.printable for c in value):
- value = value.encode("hex")
- return "%s=%s" % (name, value)
+ # TODO: use "backslashreplace" when python 2 is no longer supported.
+ value = value.rstrip(b"\x00").decode(errors="ignore")
+ elif HasNonPrintableChar(value):
+ value = binascii.hexlify(value).decode()
+ return "%s=%s" % (name, str(value))
descriptions = [
FieldDesc(i, n, v) for i, (n, v) in
diff --git a/net/test/cstruct_test.py b/net/test/cstruct_test.py
index 222ee19..da684c6 100755
--- a/net/test/cstruct_test.py
+++ b/net/test/cstruct_test.py
@@ -14,6 +14,7 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+import binascii
import unittest
import cstruct
@@ -87,9 +88,9 @@
" nest2=Nested(word1=33214, nest2=TestStructA(byte1=3, int2=4),"
" nest3=TestStructB(byte1=7, int2=33627591), int4=-55), byte3=252)")
self.assertEqual(expected, str(d))
- expected = ("01" "02000000"
- "81be" "03" "04000000"
- "07" "c71d0102" "ffffffc9" "fc").decode("hex")
+ expected = binascii.unhexlify("01" "02000000"
+ "81be" "03" "04000000"
+ "07" "c71d0102" "ffffffc9" "fc")
self.assertEqual(expected, d.Pack())
unpacked = DoubleNested(expected)
self.CheckEquals(unpacked, d)
@@ -110,6 +111,12 @@
" int3=12345, ascii4=hello\x00visible123, word5=33210)")
self.assertEqual(expected, str(t))
+ embedded_non_ascii = b"hello\xc0visible123"
+ t = TestStruct((2, embedded_non_ascii, 12345, embeddednull, 33210))
+ expected = ("TestStruct(byte1=2, string2=68656c6c6fc076697369626c65313233,"
+ " int3=12345, ascii4=hello\x00visible123, word5=33210)")
+ self.assertEqual(expected, str(t))
+
def testZeroInitialization(self):
TestStruct = cstruct.Struct("TestStruct", "B16si16AH",
"byte1 string2 int3 ascii4 word5")
@@ -124,17 +131,17 @@
def testKeywordInitialization(self):
TestStruct = cstruct.Struct("TestStruct", "=B16sIH",
"byte1 string2 int3 word4")
- text = "hello world! ^_^"
- text_bytes = text.encode("hex")
+ bytes = b"hello world! ^_^"
+ hex_bytes = binascii.hexlify(bytes)
# Populate all fields
- t1 = TestStruct(byte1=1, string2=text, int3=0xFEDCBA98, word4=0x1234)
- expected = ("01" + text_bytes + "98BADCFE" "3412").decode("hex")
+ t1 = TestStruct(byte1=1, string2=bytes, int3=0xFEDCBA98, word4=0x1234)
+ expected = binascii.unhexlify(b"01" + hex_bytes + b"98BADCFE" b"3412")
self.assertEqual(expected, t1.Pack())
# Partially populated
- t1 = TestStruct(string2=text, word4=0x1234)
- expected = ("00" + text_bytes + "00000000" "3412").decode("hex")
+ t1 = TestStruct(string2=bytes, word4=0x1234)
+ expected = binascii.unhexlify(b"00" + hex_bytes + b"00000000" b"3412")
self.assertEqual(expected, t1.Pack())
def testCstructOffset(self):