Marco Ricci commited on 2025-01-26 16:21:45
Zeige 1 geänderte Dateien mit 64 Einfügungen und 10 Löschungen.
Add more hypothesis tests for SSH string encoding and decoding, based on David R. MacIver's articles. Also document the explicit examples in the proper way.
... | ... |
@@ -819,17 +819,33 @@ class TestAgentInteraction: |
819 | 819 |
class TestHypotheses: |
820 | 820 |
"""Test properties via hypothesis.""" |
821 | 821 |
|
822 |
+ @staticmethod |
|
823 |
+ def as_ssh_string(bytestring: bytes) -> bytes: |
|
824 |
+ return int.to_bytes(len(bytestring), 4, 'big') + bytestring |
|
825 |
+ |
|
826 |
+ @staticmethod |
|
827 |
+ def canonicalize1(data: bytes) -> bytes: |
|
828 |
+ return ssh_agent.SSHAgentClient.string( |
|
829 |
+ ssh_agent.SSHAgentClient.unstring(data) |
|
830 |
+ ) |
|
831 |
+ |
|
832 |
+ @staticmethod |
|
833 |
+ def canonicalize2(data: bytes) -> bytes: |
|
834 |
+ unstringed, trailer = ssh_agent.SSHAgentClient.unstring_prefix(data) |
|
835 |
+ assert not trailer |
|
836 |
+ return ssh_agent.SSHAgentClient.string(unstringed) |
|
837 |
+ |
|
822 | 838 |
@hypothesis.given(strategies.integers(min_value=0, max_value=0xFFFFFFFF)) |
823 |
- # standard example value |
|
824 |
- @hypothesis.example(0xDEADBEEF) |
|
839 |
+ @hypothesis.example(0xDEADBEEF).via('manual, pre-hypothesis example') |
|
825 | 840 |
def test_210a_uint32_from_number(self, num: int) -> None: |
826 | 841 |
"""`uint32` encoding works, starting from numbers.""" |
827 | 842 |
uint32 = ssh_agent.SSHAgentClient.uint32 |
828 | 843 |
assert int.from_bytes(uint32(num), 'big', signed=False) == num |
829 | 844 |
|
830 | 845 |
@hypothesis.given(strategies.binary(min_size=4, max_size=4)) |
831 |
- # standard example value |
|
832 |
- @hypothesis.example(b'\xde\xad\xbe\xef') |
|
846 |
+ @hypothesis.example(b'\xde\xad\xbe\xef').via( |
|
847 |
+ 'manual, pre-hypothesis example' |
|
848 |
+ ) |
|
833 | 849 |
def test_210b_uint32_from_bytestring(self, bytestring: bytes) -> None: |
834 | 850 |
"""`uint32` encoding works, starting from length four byte strings.""" |
835 | 851 |
uint32 = ssh_agent.SSHAgentClient.uint32 |
... | ... |
@@ -839,8 +855,9 @@ class TestHypotheses: |
839 | 855 |
) |
840 | 856 |
|
841 | 857 |
@hypothesis.given(strategies.binary(max_size=0x0001FFFF)) |
842 |
- # example: highest order bit is set |
|
843 |
- @hypothesis.example(b'DEADBEEF' * 10000) |
|
858 |
+ @hypothesis.example(b'DEADBEEF' * 10000).via( |
|
859 |
+ 'manual, pre-hypothesis example with highest order bit set' |
|
860 |
+ ) |
|
844 | 861 |
def test_211a_string_from_bytestring(self, bytestring: bytes) -> None: |
845 | 862 |
"""SSH string encoding works, starting from a byte string.""" |
846 | 863 |
res = ssh_agent.SSHAgentClient.string(bytestring) |
... | ... |
@@ -849,10 +866,22 @@ class TestHypotheses: |
849 | 866 |
assert res[4:] == bytestring |
850 | 867 |
|
851 | 868 |
@hypothesis.given(strategies.binary(max_size=0x00FFFFFF)) |
852 |
- # example: check for double-deserialization |
|
853 |
- @hypothesis.example(b'\x00\x00\x00\x07ssh-rsa') |
|
854 |
- def test_212_string_unstring(self, bytestring: bytes) -> None: |
|
855 |
- """SSH string decoding of encoded SSH strings works.""" |
|
869 |
+ @hypothesis.example(b'\x00\x00\x00\x07ssh-rsa').via( |
|
870 |
+ 'manual, pre-hypothesis example to attempt to detect double-decoding' |
|
871 |
+ ) |
|
872 |
+ @hypothesis.example(b'\x00\x00\x00\x01').via( |
|
873 |
+ 'detect no-op encoding via ill-formed SSH string' |
|
874 |
+ ) |
|
875 |
+ def test_212a_unstring_of_string_of_data(self, bytestring: bytes) -> None: |
|
876 |
+ """SSH string decoding of encoded SSH strings works. |
|
877 |
+ |
|
878 |
+ References: |
|
879 |
+ |
|
880 |
+ * [David R. MacIver: The Encode/Decode invariant][ENCODE_DECODE] |
|
881 |
+ |
|
882 |
+ [ENCODE_DECODE]: https://hypothesis.works/articles/encode-decode-invariant/ |
|
883 |
+ |
|
884 |
+ """ |
|
856 | 885 |
string = ssh_agent.SSHAgentClient.string |
857 | 886 |
unstring = ssh_agent.SSHAgentClient.unstring |
858 | 887 |
unstring_prefix = ssh_agent.SSHAgentClient.unstring_prefix |
... | ... |
@@ -862,3 +891,28 @@ class TestHypotheses: |
862 | 891 |
trailing_data = b' trailing data' |
863 | 892 |
encoded2 = string(bytestring) + trailing_data |
864 | 893 |
assert unstring_prefix(encoded2) == (bytestring, trailing_data) |
894 |
+ |
|
895 |
+ @hypothesis.given( |
|
896 |
+ strategies.binary(max_size=0x00FFFFFF).map( |
|
897 |
+ # Scoping issues, and the fact that staticmethod objects |
|
898 |
+ # (before class finalization) are not callable, necessitate |
|
899 |
+ # wrapping this staticmethod call in a lambda. |
|
900 |
+ lambda x: TestHypotheses.as_ssh_string(x) # noqa: PLW0108 |
|
901 |
+ ), |
|
902 |
+ ) |
|
903 |
+ def test_212b_string_of_unstring_of_data(self, encoded: bytes) -> None: |
|
904 |
+ """SSH string decoding of encoded SSH strings works. |
|
905 |
+ |
|
906 |
+ References: |
|
907 |
+ |
|
908 |
+ * [David R. MacIver: Another invariant to test for |
|
909 |
+ encoders][DECODE_ENCODE] |
|
910 |
+ |
|
911 |
+ [DECODE_ENCODE]: https://hypothesis.works/articles/canonical-serialization/ |
|
912 |
+ |
|
913 |
+ """ |
|
914 |
+ canonical_functions = [self.canonicalize1, self.canonicalize2] |
|
915 |
+ for canon1 in canonical_functions: |
|
916 |
+ for canon2 in canonical_functions: |
|
917 |
+ assert canon1(encoded) == canon2(encoded) |
|
918 |
+ assert canon1(canon2(encoded)) == canon1(encoded) |
|
865 | 919 |