laforge has uploaded this change for review. ( https://gerrit.osmocom.org/c/python/pyosmocom/+/38023?usp=email )
Change subject: Introduce a new 'hexstr' type to represent hex-strings ......................................................................
Introduce a new 'hexstr' type to represent hex-strings
hexstr differs from str in that comparisons are case-insensitive, and it offers encoding-free conversion from hexstr to bytes and vice-versa.
Change-Id: I16c0df809bc11ec0f98e8ade404f9b82072e3a06 --- M src/osmocom/utils.py A tests/test_utils.py 2 files changed, 88 insertions(+), 0 deletions(-)
git pull ssh://gerrit.osmocom.org:29418/python/pyosmocom refs/changes/23/38023/1
diff --git a/src/osmocom/utils.py b/src/osmocom/utils.py index cbcdd46..96476f1 100644 --- a/src/osmocom/utils.py +++ b/src/osmocom/utils.py @@ -138,6 +138,36 @@ except: return False
+class hexstr(str): + """Class derived from 'str', represeting a string of hexadecimal digits. It differs in that + comparisons are case-insensitive, and it offers encoding-free conversion from hexstr to bytes + and vice-versa.""" + def __new__(cls, s: str): + if not all(c in string.hexdigits for c in s): + raise ValueError('Input must be hexadecimal digits only') + # store as lower case digits + return super().__new__(cls, s.lower()) + + def __eq__(self, other): + # make sure comparison is done case-insensitive + return str(self) == other.lower() + + def __getitem__(self, val): + # make sure slicing a hexstr will return a hexstr + return hexstr(super().__getitem__(val)) + + def to_bytes(self): + """return hex-string converted to bytes""" + s = str(self) + if len(s) & 1: + raise ValueError('Cannot convert hex string with odd number of digits') + return h2b(s) + + @classmethod + def from_bytes(cls, bt: bytes) -> 'hexstr': + """instantiate hex-string from bytes""" + return cls(b2h(bt)) + ######################################################################### # ARGPARSE HELPERS ######################################################################### diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100755 index 0000000..8e4732f --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 + +# (C) 2022 by Harald Welte laforge@osmocom.org +# All Rights Reserved +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see http://www.gnu.org/licenses/. + +import unittest + +from osmocom.utils import * + +class TestHexstr(unittest.TestCase): + def test_cmp(self): + s = hexstr('aBcD') + self.assertEqual(s, 'abcd') + self.assertEqual(s, 'ABCD') + self.assertEqual(s, 'AbCd') + self.assertEqual(s, hexstr('AbCd')) + + def test_tobytes(self): + s = hexstr('aBcDeF') + self.assertEqual(s.to_bytes(), b'\xab\xcd\xef') + + def test_tobytes_odd(self): + s2 = hexstr('aBc') + with self.assertRaises(ValueError): + s2.to_bytes() + + def test_frombytes(self): + s = hexstr.from_bytes(b'\x01\x02\xaa') + self.assertEqual(s, '0102aa') + + def test_slice(self): + s = hexstr('abcdef') + slice1 = s[-2:] + self.assertTrue(isinstance(slice1, hexstr)) + self.assertEqual(slice1, 'ef') + slice2 = s[1] + self.assertTrue(isinstance(slice2, hexstr)) + self.assertEqual(slice2, 'b') + + def test_str_lower(self): + self.assertEqual(str(hexstr('ABCD')), 'abcd') + + +if __name__ == "__main__": + unittest.main()