This is merely a historical archive of years 2008-2021, before the migration to mailman3.
A maintained and still updated list archive can be found at https://lists.osmocom.org/hyperkitty/list/gerrit-log@lists.osmocom.org/.
laforge gerrit-no-reply at lists.osmocom.orglaforge has submitted this change. ( https://gerrit.osmocom.org/c/pysim/+/23577 ) Change subject: Add more documentation to the classes/methods ...................................................................... Add more documentation to the classes/methods * add type annotations in-line with PEP484 * convert existing documentation to follow the "Google Python Style Guide" format understood by the sphinx.ext.napoleon' extension * add much more documentation all over the code base Change-Id: I6ac88e0662cf3c56ae32d86d50b18a8b4150571a --- M pySim/commands.py M pySim/exceptions.py M pySim/filesystem.py M pySim/transport/__init__.py M pySim/transport/calypso.py M pySim/transport/modem_atcmd.py M pySim/transport/pcsc.py M pySim/transport/serial.py M pySim/utils.py 9 files changed, 635 insertions(+), 185 deletions(-) Approvals: Jenkins Builder: Verified laforge: Looks good to me, approved diff --git a/pySim/commands.py b/pySim/commands.py index 5184a77..7919099 100644 --- a/pySim/commands.py +++ b/pySim/commands.py @@ -65,7 +65,7 @@ # from what SIMs responds. See also: # USIM: ETSI TS 102 221, chapter 11.1.1.3 Response Data # SIM: GSM 11.11, chapter 9.2.1 SELECT - def __record_len(self, r): + def __record_len(self, r) -> int: if self.sel_ctrl == "0004": tlv_parsed = self.__parse_fcp(r[-1]) file_descriptor = tlv_parsed['82'] @@ -76,14 +76,15 @@ # Tell the length of a binary file. See also comment # above. - def __len(self, r): + def __len(self, r) -> int: if self.sel_ctrl == "0004": tlv_parsed = self.__parse_fcp(r[-1]) return int(tlv_parsed['80'], 16) else: return int(r[-1][4:8], 16) - def get_atr(self): + def get_atr(self) -> str: + """Return the ATR of the currently inserted card.""" return self._tp.get_atr() @property @@ -101,6 +102,7 @@ self._sel_ctrl = value def try_select_path(self, dir_list): + """ Try to select a specified path given as list of hex-string FIDs""" rv = [] if type(dir_list) is not list: dir_list = [dir_list] @@ -112,6 +114,14 @@ return rv def select_path(self, dir_list): + """Execute SELECT for an entire list/path of FIDs. + + Args: + dir_list: list of FIDs representing the path to select + + Returns: + list of return values (FCP in hex encoding) for each element of the path + """ rv = [] if type(dir_list) is not list: dir_list = [dir_list] @@ -120,14 +130,23 @@ rv.append(data) return rv - def select_file(self, fid): + def select_file(self, fid:str): + """Execute SELECT a given file by FID.""" return self._tp.send_apdu_checksw(self.cla_byte + "a4" + self.sel_ctrl + "02" + fid) - def select_adf(self, aid): + def select_adf(self, aid:str): + """Execute SELECT a given Applicaiton ADF.""" aidlen = ("0" + format(len(aid) // 2, 'x'))[-2:] return self._tp.send_apdu_checksw(self.cla_byte + "a4" + "0404" + aidlen + aid) - def read_binary(self, ef, length=None, offset=0): + def read_binary(self, ef, length:int=None, offset:int=0): + """Execute READD BINARY. + + Args: + ef : string or list of strings indicating name or path of transparent EF + length : number of bytes to read + offset : byte offset in file from which to start reading + """ r = self.select_path(ef) if len(r[-1]) == 0: return (None, None) @@ -145,7 +164,15 @@ raise ValueError('Failed to read (offset %d)' % (offset)) return total_data, sw - def update_binary(self, ef, data, offset=0, verify=False, conserve=False): + def update_binary(self, ef, data:str, offset:int=0, verify:bool=False, conserve:bool=False): + """Execute UPDATE BINARY. + + Args: + ef : string or list of strings indicating name or path of transparent EF + data : hex string of data to be written + offset : byte offset in file from which to start writing + verify : Whether or not to verify data after write + """ data_length = len(data) // 2 # Save write cycles by reading+comparing before write @@ -161,18 +188,32 @@ self.verify_binary(ef, data, offset) return res - def verify_binary(self, ef, data, offset=0): + def verify_binary(self, ef, data:str, offset:int=0): + """Verify contents of transparent EF. + + Args: + ef : string or list of strings indicating name or path of transparent EF + data : hex string of expected data + offset : byte offset in file from which to start verifying + """ res = self.read_binary(ef, len(data) // 2, offset) if res[0].lower() != data.lower(): raise ValueError('Binary verification failed (expected %s, got %s)' % (data.lower(), res[0].lower())) - def read_record(self, ef, rec_no): + def read_record(self, ef, rec_no:int): + """Execute READ RECORD. + + Args: + ef : string or list of strings indicating name or path of linear fixed EF + rec_no : record number to read + """ r = self.select_path(ef) rec_length = self.__record_len(r) pdu = self.cla_byte + 'b2%02x04%02x' % (rec_no, rec_length) return self._tp.send_apdu(pdu) - def update_record(self, ef, rec_no, data, force_len=False, verify=False, conserve=False): + def update_record(self, ef, rec_no:int, data:str, force_len:bool=False, verify:bool=False, + conserve:bool=False): r = self.select_path(ef) if not force_len: rec_length = self.__record_len(r) @@ -194,30 +235,47 @@ self.verify_record(ef, rec_no, data) return res - def verify_record(self, ef, rec_no, data): + def verify_record(self, ef, rec_no:int, data:str): res = self.read_record(ef, rec_no) if res[0].lower() != data.lower(): raise ValueError('Record verification failed (expected %s, got %s)' % (data.lower(), res[0].lower())) def record_size(self, ef): + """Determine the record size of given file. + + Args: + ef : string or list of strings indicating name or path of linear fixed EF + """ r = self.select_path(ef) return self.__record_len(r) def record_count(self, ef): + """Determine the number of records in given file. + + Args: + ef : string or list of strings indicating name or path of linear fixed EF + """ r = self.select_path(ef) return self.__len(r) // self.__record_len(r) def binary_size(self, ef): + """Determine the size of given transparent file. + + Args: + ef : string or list of strings indicating name or path of transparent EF + """ r = self.select_path(ef) return self.__len(r) - def run_gsm(self, rand): + def run_gsm(self, rand:str): + """Execute RUN GSM ALGORITHM.""" if len(rand) != 32: raise ValueError('Invalid rand') self.select_path(['3f00', '7f20']) return self._tp.send_apdu(self.cla_byte + '88000010' + rand) def reset_card(self): + """Physically reset the card""" return self._tp.reset_card() def _chv_process_sw(self, op_name, chv_no, pin_code, sw): @@ -227,31 +285,36 @@ elif (sw != '9000'): raise SwMatchError(sw, '9000') - def verify_chv(self, chv_no, pin_code): - fc = rpad(b2h(pin_code), 16) + def verify_chv(self, chv_no:int, code:str): + """Verify a given CHV (Card Holder Verification == PIN)""" + fc = rpad(b2h(code), 16) data, sw = self._tp.send_apdu(self.cla_byte + '2000' + ('%02X' % chv_no) + '08' + fc) - self._chv_process_sw('verify', chv_no, pin_code, sw) + self._chv_process_sw('verify', chv_no, code, sw) return (data, sw) - def unblock_chv(self, chv_no, puk_code, pin_code): + def unblock_chv(self, chv_no:int, puk_code:str, pin_code:str): + """Unblock a given CHV (Card Holder Verification == PIN)""" fc = rpad(b2h(puk_code), 16) + rpad(b2h(pin_code), 16) data, sw = self._tp.send_apdu(self.cla_byte + '2C00' + ('%02X' % chv_no) + '10' + fc) self._chv_process_sw('unblock', chv_no, pin_code, sw) return (data, sw) - def change_chv(self, chv_no, pin_code, new_pin_code): + def change_chv(self, chv_no:int, pin_code:str, new_pin_code:str): + """Change a given CHV (Card Holder Verification == PIN)""" fc = rpad(b2h(pin_code), 16) + rpad(b2h(new_pin_code), 16) data, sw = self._tp.send_apdu(self.cla_byte + '2400' + ('%02X' % chv_no) + '10' + fc) self._chv_process_sw('change', chv_no, pin_code, sw) return (data, sw) - def disable_chv(self, chv_no, pin_code): + def disable_chv(self, chv_no:int, pin_code:str): + """Disable a given CHV (Card Holder Verification == PIN)""" fc = rpad(b2h(pin_code), 16) data, sw = self._tp.send_apdu(self.cla_byte + '2600' + ('%02X' % chv_no) + '08' + fc) self._chv_process_sw('disable', chv_no, pin_code, sw) return (data, sw) - def enable_chv(self, chv_no, pin_code): + def enable_chv(self, chv_no:int, pin_code:str): + """Enable a given CHV (Card Holder Verification == PIN)""" fc = rpad(b2h(pin_code), 16) data, sw = self._tp.send_apdu(self.cla_byte + '2800' + ('%02X' % chv_no) + '08' + fc) self._chv_process_sw('enable', chv_no, pin_code, sw) diff --git a/pySim/exceptions.py b/pySim/exceptions.py index 4fb8f72..f1d1a18 100644 --- a/pySim/exceptions.py +++ b/pySim/exceptions.py @@ -22,18 +22,27 @@ # class NoCardError(Exception): + """No card was found in the reader.""" pass class ProtocolError(Exception): + """Some kind of protocol level error interfacing with the card.""" pass class ReaderError(Exception): + """Some kind of general error with the card reader.""" pass class SwMatchError(Exception): """Raised when an operation specifies an expected SW but the actual SW from the card doesn't match.""" - def __init__(self, sw_actual, sw_expected, rs=None): + def __init__(self, sw_actual:str, sw_expected:str, rs=None): + """ + Args: + sw_actual : the SW we actually received from the card (4 hex digits) + sw_expected : the SW we expected to receive from the card (4 hex digits) + rs : interpreter class to convert SW to string + """ self.sw_actual = sw_actual self.sw_expected = sw_expected self.rs = rs diff --git a/pySim/filesystem.py b/pySim/filesystem.py index e959c52..d1a7b51 100644 --- a/pySim/filesystem.py +++ b/pySim/filesystem.py @@ -31,6 +31,8 @@ from cmd2 import CommandSet, with_default_category, with_argparser import argparse +from typing import Optional, Iterable, List, Any, Tuple + from pySim.utils import sw_match, h2b, b2h, is_hex from pySim.exceptions import * @@ -41,7 +43,16 @@ RESERVED_NAMES = ['..', '.', '/', 'MF'] RESERVED_FIDS = ['3f00'] - def __init__(self, fid=None, sfid=None, name=None, desc=None, parent=None): + def __init__(self, fid:str=None, sfid:str=None, name:str=None, desc:str=None, + parent:Optional['CardDF']=None): + """ + Args: + fid : File Identifier (4 hex digits) + sfid : Short File Identifier (2 hex digits, optional) + name : Brief name of the file, lik EF_ICCID + desc : Descriptoin of the file + parent : Parent CardFile object within filesystem hierarchy + """ if not isinstance(self, CardADF) and fid == None: raise ValueError("fid is mandatory") if fid: @@ -53,7 +64,7 @@ self.parent = parent if self.parent and self.parent != self and self.fid: self.parent.add_file(self) - self.shell_commands = [] + self.shell_commands: List[CommandSet] = [] # Note: the basic properties (fid, name, ect.) are verified when # the file is attached to a parent file. See method add_file() in @@ -65,14 +76,18 @@ else: return self.fid - def _path_element(self, prefer_name): + def _path_element(self, prefer_name:bool) -> Optional[str]: if prefer_name and self.name: return self.name else: return self.fid - def fully_qualified_path(self, prefer_name=True): - """Return fully qualified path to file as list of FID or name strings.""" + def fully_qualified_path(self, prefer_name:bool=True): + """Return fully qualified path to file as list of FID or name strings. + + Args: + prefer_name : Preferably build path of names; fall-back to FIDs as required + """ if self.parent != self: ret = self.parent.fully_qualified_path(prefer_name) else: @@ -80,7 +95,7 @@ ret.append(self._path_element(prefer_name)) return ret - def get_mf(self): + def get_mf(self) -> Optional['CardMF']: """Return the MF (root) of the file system.""" if self.parent == None: return None @@ -90,8 +105,16 @@ node = node.parent return node - def _get_self_selectables(self, alias=None, flags = []): - """Return a dict of {'identifier': self} tuples""" + def _get_self_selectables(self, alias:str=None, flags = []) -> dict: + """Return a dict of {'identifier': self} tuples. + + Args: + alias : Add an alias with given name to 'self' + flags : Specify which selectables to return 'FIDS' and/or 'NAMES'; + If not specified, all selectables will be returned. + Returns: + dict containing reference to 'self' for all identifiers. + """ sels = {} if alias: sels.update({alias: self}) @@ -101,8 +124,16 @@ sels.update({self.name: self}) return sels - def get_selectables(self, flags = []): - """Return a dict of {'identifier': File} that is selectable from the current file.""" + def get_selectables(self, flags = []) -> dict: + """Return a dict of {'identifier': File} that is selectable from the current file. + + Args: + flags : Specify which selectables to return 'FIDS' and/or 'NAMES'; + If not specified, all selectables will be returned. + Returns: + dict containing all selectable items. Key is identifier (string), value + a reference to a CardFile (or derived class) instance. + """ sels = {} # we can always select ourself if flags == [] or 'SELF' in flags: @@ -118,12 +149,20 @@ sels.update(mf.get_app_selectables(flags = flags)) return sels - def get_selectable_names(self, flags = []): - """Return a list of strings for all identifiers that are selectable from the current file.""" + def get_selectable_names(self, flags = []) -> dict: + """Return a dict of {'identifier': File} that is selectable from the current file. + + Args: + flags : Specify which selectables to return 'FIDS' and/or 'NAMES'; + If not specified, all selectables will be returned. + Returns: + dict containing all selectable items. Key is identifier (string), value + a reference to a CardFile (or derived class) instance. + """ sels = self.get_selectables(flags) return sels.keys() - def decode_select_response(self, data_hex): + def decode_select_response(self, data_hex:str): """Decode the response to a SELECT command.""" return self.parent.decode_select_response(data_hex) @@ -147,8 +186,12 @@ def __str__(self): return "DF(%s)" % (super().__str__()) - def add_file(self, child, ignore_existing=False): - """Add a child (DF/EF) to this DF""" + def add_file(self, child:CardFile, ignore_existing:bool=False): + """Add a child (DF/EF) to this DF. + Args: + child: The new DF/EF to be added + ignore_existing: Ignore, if file with given FID already exists. Old one will be kept. + """ if not isinstance(child, CardFile): raise TypeError("Expected a File instance") if not is_hex(child.fid, minlen = 4, maxlen = 4): @@ -170,13 +213,26 @@ self.children[child.fid] = child child.parent = self - def add_files(self, children, ignore_existing=False): - """Add a list of child (DF/EF) to this DF""" + def add_files(self, children:Iterable[CardFile], ignore_existing:bool=False): + """Add a list of child (DF/EF) to this DF + + Args: + children: List of new DF/EFs to be added + ignore_existing: Ignore, if file[s] with given FID already exists. Old one[s] will be kept. + """ for child in children: self.add_file(child, ignore_existing) - def get_selectables(self, flags = []): - """Get selectable (DF/EF names) from current DF""" + def get_selectables(self, flags = []) -> dict: + """Return a dict of {'identifier': File} that is selectable from the current DF. + + Args: + flags : Specify which selectables to return 'FIDS' and/or 'NAMES'; + If not specified, all selectables will be returned. + Returns: + dict containing all selectable items. Key is identifier (string), value + a reference to a CardFile (or derived class) instance. + """ # global selectables + our children sels = super().get_selectables(flags) if flags == [] or 'FIDS' in flags: @@ -185,7 +241,8 @@ sels.update({x.name: x for x in self.children.values() if x.name}) return sels - def lookup_file_by_name(self, name): + def lookup_file_by_name(self, name:str) -> Optional[CardFile]: + """Find a file with given name within current DF.""" if name == None: return None for i in self.children.values(): @@ -193,7 +250,8 @@ return i return None - def lookup_file_by_sfid(self, sfid): + def lookup_file_by_sfid(self, sfid:str) -> Optional[CardFile]: + """Find a file with given short file ID within current DF.""" if sfid == None: return None for i in self.children.values(): @@ -201,7 +259,8 @@ return i return None - def lookup_file_by_fid(self, fid): + def lookup_file_by_fid(self, fid:str) -> Optional[CardFile]: + """Find a file with given file ID within current DF.""" if fid in self.children: return self.children[fid] return None @@ -222,7 +281,7 @@ def __str__(self): return "MF(%s)" % (self.fid) - def add_application(self, app): + def add_application(self, app:'CardADF'): """Add an ADF (Application Dedicated File) to the MF""" if not isinstance(app, CardADF): raise TypeError("Expected an ADF instance") @@ -235,13 +294,21 @@ """Get list of completions (AID names)""" return [x.name for x in self.applications] - def get_selectables(self, flags = []): - """Get list of completions (DF/EF/ADF names) from current DF""" + def get_selectables(self, flags = []) -> dict: + """Return a dict of {'identifier': File} that is selectable from the current DF. + + Args: + flags : Specify which selectables to return 'FIDS' and/or 'NAMES'; + If not specified, all selectables will be returned. + Returns: + dict containing all selectable items. Key is identifier (string), value + a reference to a CardFile (or derived class) instance. + """ sels = super().get_selectables(flags) sels.update(self.get_app_selectables(flags)) return sels - def get_app_selectables(self, flags = []): + def get_app_selectables(self, flags = []) -> dict: """Get applications by AID + name""" sels = {} if flags == [] or 'AIDS' in flags: @@ -250,15 +317,19 @@ sels.update({x.name: x for x in self.applications.values() if x.name}) return sels - def decode_select_response(self, data_hex): - """Decode the response to a SELECT command.""" + def decode_select_response(self, data_hex:str) -> Any: + """Decode the response to a SELECT command. + + This is the fall-back method which doesn't perform any decoding. It mostly + exists so specific derived classes can overload it for actual decoding. + """ return data_hex class CardADF(CardDF): """ADF (Application Dedicated File) in the smart card filesystem""" - def __init__(self, aid, **kwargs): + def __init__(self, aid:str, **kwargs): super().__init__(**kwargs) self.aid = aid # Application Identifier if self.parent: @@ -267,7 +338,7 @@ def __str__(self): return "ADF(%s)" % (self.aid) - def _path_element(self, prefer_name): + def _path_element(self, prefer_name:bool): if self.name and prefer_name: return self.name else: @@ -283,8 +354,16 @@ def __str__(self): return "EF(%s)" % (super().__str__()) - def get_selectables(self, flags = []): - """Get list of completions (EF names) from current DF""" + def get_selectables(self, flags = []) -> dict: + """Return a dict of {'identifier': File} that is selectable from the current DF. + + Args: + flags : Specify which selectables to return 'FIDS' and/or 'NAMES'; + If not specified, all selectables will be returned. + Returns: + dict containing all selectable items. Key is identifier (string), value + a reference to a CardFile (or derived class) instance. + """ #global selectable names + those of the parent DF sels = super().get_selectables(flags) sels.update({x.name:x for x in self.parent.children.values() if x != self}) @@ -292,10 +371,14 @@ class TransparentEF(CardEF): - """Transparent EF (Entry File) in the smart card filesystem""" + """Transparent EF (Entry File) in the smart card filesystem. + + A Transparent EF is a binary file with no formal structure. This is contrary to + Record based EFs which have [fixed size] records that can be individually read/updated.""" @with_default_category('Transparent EF Commands') class ShellCommands(CommandSet): + """Shell commands specific for Trransparent EFs.""" def __init__(self): super().__init__() @@ -333,13 +416,33 @@ if data: self._cmd.poutput(json.dumps(data, indent=4)) - def __init__(self, fid, sfid=None, name=None, desc=None, parent=None, size={1,None}): + def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, parent:CardDF=None, + size={1,None}): + """ + Args: + fid : File Identifier (4 hex digits) + sfid : Short File Identifier (2 hex digits, optional) + name : Brief name of the file, lik EF_ICCID + desc : Descriptoin of the file + parent : Parent CardFile object within filesystem hierarchy + size : tuple of (minimum_size, recommended_size) + """ super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent) self.size = size self.shell_commands = [self.ShellCommands()] - def decode_bin(self, raw_bin_data): - """Decode raw (binary) data into abstract representation. Overloaded by specific classes.""" + def decode_bin(self, raw_bin_data:bytearray) -> dict: + """Decode raw (binary) data into abstract representation. + + A derived class would typically provide a _decode_bin() or _decode_hex() method + for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + raw_bin_data : binary encoded data + Returns: + abstract_data; dict representing the decoded data + """ method = getattr(self, '_decode_bin', None) if callable(method): return method(raw_bin_data) @@ -348,8 +451,18 @@ return method(b2h(raw_bin_data)) return {'raw': raw_bin_data.hex()} - def decode_hex(self, raw_hex_data): - """Decode raw (hex string) data into abstract representation. Overloaded by specific classes.""" + def decode_hex(self, raw_hex_data:str) -> dict: + """Decode raw (hex string) data into abstract representation. + + A derived class would typically provide a _decode_bin() or _decode_hex() method + for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + raw_hex_data : hex-encoded data + Returns: + abstract_data; dict representing the decoded data + """ method = getattr(self, '_decode_hex', None) if callable(method): return method(raw_hex_data) @@ -359,8 +472,18 @@ return method(raw_bin_data) return {'raw': raw_bin_data.hex()} - def encode_bin(self, abstract_data): - """Encode abstract representation into raw (binary) data. Overloaded by specific classes.""" + def encode_bin(self, abstract_data:dict) -> bytearray: + """Encode abstract representation into raw (binary) data. + + A derived class would typically provide an _encode_bin() or _encode_hex() method + for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + abstract_data : dict representing the decoded data + Returns: + binary encoded data + """ method = getattr(self, '_encode_bin', None) if callable(method): return method(abstract_data) @@ -369,8 +492,18 @@ return h2b(method(abstract_data)) raise NotImplementedError - def encode_hex(self, abstract_data): - """Encode abstract representation into raw (hex string) data. Overloaded by specific classes.""" + def encode_hex(self, abstract_data:dict) -> str: + """Encode abstract representation into raw (hex string) data. + + A derived class would typically provide an _encode_bin() or _encode_hex() method + for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + abstract_data : dict representing the decoded data + Returns: + hex string encoded data + """ method = getattr(self, '_encode_hex', None) if callable(method): return method(abstract_data) @@ -382,10 +515,14 @@ class LinFixedEF(CardEF): - """Linear Fixed EF (Entry File) in the smart card filesystem""" + """Linear Fixed EF (Entry File) in the smart card filesystem. + + Linear Fixed EFs are record oriented files. They consist of a number of fixed-size + records. The records can be individually read/updated.""" @with_default_category('Linear Fixed EF Commands') class ShellCommands(CommandSet): + """Shell commands specific for Linear Fixed EFs.""" def __init__(self): super().__init__() @@ -432,13 +569,33 @@ if data: self._cmd.poutput(data) - def __init__(self, fid, sfid=None, name=None, desc=None, parent=None, rec_len={1,None}): + def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, + parent:Optional[CardDF]=None, rec_len={1,None}): + """ + Args: + fid : File Identifier (4 hex digits) + sfid : Short File Identifier (2 hex digits, optional) + name : Brief name of the file, lik EF_ICCID + desc : Descriptoin of the file + parent : Parent CardFile object within filesystem hierarchy + rec_len : tuple of (minimum_length, recommended_length) + """ super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent) self.rec_len = rec_len self.shell_commands = [self.ShellCommands()] - def decode_record_hex(self, raw_hex_data): - """Decode raw (hex string) data into abstract representation. Overloaded by specific classes.""" + def decode_record_hex(self, raw_hex_data:str) -> dict: + """Decode raw (hex string) data into abstract representation. + + A derived class would typically provide a _decode_record_bin() or _decode_record_hex() + method for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + raw_hex_data : hex-encoded data + Returns: + abstract_data; dict representing the decoded data + """ method = getattr(self, '_decode_record_hex', None) if callable(method): return method(raw_hex_data) @@ -448,8 +605,18 @@ return method(raw_bin_data) return {'raw': raw_bin_data.hex()} - def decode_record_bin(self, raw_bin_data): - """Decode raw (binary) data into abstract representation. Overloaded by specific classes.""" + def decode_record_bin(self, raw_bin_data:bytearray) -> dict: + """Decode raw (binary) data into abstract representation. + + A derived class would typically provide a _decode_record_bin() or _decode_record_hex() + method for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + raw_bin_data : binary encoded data + Returns: + abstract_data; dict representing the decoded data + """ method = getattr(self, '_decode_record_bin', None) if callable(method): return method(raw_bin_data) @@ -459,47 +626,90 @@ return method(raw_hex_data) return {'raw': raw_hex_data} - def encode_record_hex(self, abstract_data): - """Encode abstract representation into raw (hex string) data. Overloaded by specific classes.""" + def encode_record_hex(self, abstract_data:dict) -> str: + """Encode abstract representation into raw (hex string) data. + + A derived class would typically provide an _encode_record_bin() or _encode_record_hex() + method for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + abstract_data : dict representing the decoded data + Returns: + hex string encoded data + """ method = getattr(self, '_encode_record_hex', None) if callable(method): return method(abstract_data) method = getattr(self, '_encode_record_bin', None) if callable(method): raw_bin_data = method(abstract_data) - return b2h(raww_bin_data) + return h2b(raw_bin_data) raise NotImplementedError - def encode_record_bin(self, abstract_data): - """Encode abstract representation into raw (binary) data. Overloaded by specific classes.""" + def encode_record_bin(self, abstract_data:dict) -> bytearray: + """Encode abstract representation into raw (binary) data. + + A derived class would typically provide an _encode_record_bin() or _encode_record_hex() + method for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + abstract_data : dict representing the decoded data + Returns: + binary encoded data + """ method = getattr(self, '_encode_record_bin', None) if callable(method): return method(abstract_data) method = getattr(self, '_encode_record_hex', None) if callable(method): - return b2h(method(abstract_data)) + return h2b(method(abstract_data)) raise NotImplementedError class CyclicEF(LinFixedEF): """Cyclic EF (Entry File) in the smart card filesystem""" # we don't really have any special support for those; just recycling LinFixedEF here - def __init__(self, fid, sfid=None, name=None, desc=None, parent=None, rec_len={1,None}): + def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, parent:CardDF=None, + rec_len={1,None}): super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, rec_len=rec_len) class TransRecEF(TransparentEF): """Transparent EF (Entry File) containing fixed-size records. + These are the real odd-balls and mostly look like mistakes in the specification: Specified as 'transparent' EF, but actually containing several fixed-length records inside. We add a special class for those, so the user only has to provide encoder/decoder functions for a record, while this class takes care of split / merge of records. """ - def __init__(self, fid, sfid=None, name=None, desc=None, parent=None, rec_len=None, size={1,None}): + def __init__(self, fid:str, sfid:str=None, name:str=None, desc:str=None, + parent:Optional[CardDF]=None, rec_len:int=None, size={1,None}): + """ + Args: + fid : File Identifier (4 hex digits) + sfid : Short File Identifier (2 hex digits, optional) + name : Brief name of the file, lik EF_ICCID + desc : Descriptoin of the file + parent : Parent CardFile object within filesystem hierarchy + rec_len : Length of the fixed-length records within transparent EF + size : tuple of (minimum_size, recommended_size) + """ super().__init__(fid=fid, sfid=sfid, name=name, desc=desc, parent=parent, size=size) self.rec_len = rec_len - def decode_record_hex(self, raw_hex_data): - """Decode raw (hex string) data into abstract representation. Overloaded by specific classes.""" + def decode_record_hex(self, raw_hex_data:str) -> dict: + """Decode raw (hex string) data into abstract representation. + + A derived class would typically provide a _decode_record_bin() or _decode_record_hex() + method for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + raw_hex_data : hex-encoded data + Returns: + abstract_data; dict representing the decoded data + """ method = getattr(self, '_decode_record_hex', None) if callable(method): return method(raw_hex_data) @@ -509,8 +719,18 @@ return method(raw_bin_data) return {'raw': raw_hex_data} - def decode_record_bin(self, raw_bin_data): - """Decode raw (hex string) data into abstract representation. Overloaded by specific classes.""" + def decode_record_bin(self, raw_bin_data:bytearray) -> dict: + """Decode raw (binary) data into abstract representation. + + A derived class would typically provide a _decode_record_bin() or _decode_record_hex() + method for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + raw_bin_data : binary encoded data + Returns: + abstract_data; dict representing the decoded data + """ method = getattr(self, '_decode_record_bin', None) if callable(method): return method(raw_bin_data) @@ -520,8 +740,18 @@ return method(raw_hex_data) return {'raw': raw_hex_data} - def encode_record_hex(self, abstract_data): - """Encode abstract representation into raw (hex string) data. Overloaded by specific classes.""" + def encode_record_hex(self, abstract_data:dict) -> str: + """Encode abstract representation into raw (hex string) data. + + A derived class would typically provide an _encode_record_bin() or _encode_record_hex() + method for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + abstract_data : dict representing the decoded data + Returns: + hex string encoded data + """ method = getattr(self, '_encode_record_hex', None) if callable(method): return method(abstract_data) @@ -530,8 +760,18 @@ return h2b(method(abstract_data)) raise NotImplementedError - def encode_record_bin(self, abstract_data): - """Encode abstract representation into raw (binary) data. Overloaded by specific classes.""" + def encode_record_bin(self, abstract_data:dict) -> bytearray: + """Encode abstract representation into raw (binary) data. + + A derived class would typically provide an _encode_record_bin() or _encode_record_hex() + method for implementing this specifically for the given file. This function checks which + of the method exists, add calls them (with conversion, as needed). + + Args: + abstract_data : dict representing the decoded data + Returns: + binary encoded data + """ method = getattr(self, '_encode_record_bin', None) if callable(method): return method(abstract_data) @@ -540,11 +780,11 @@ return h2b(method(abstract_data)) raise NotImplementedError - def _decode_bin(self, raw_bin_data): + def _decode_bin(self, raw_bin_data:bytearray): chunks = [raw_bin_data[i:i+self.rec_len] for i in range(0, len(raw_bin_data), self.rec_len)] return [self.decode_record_bin(x) for x in chunks] - def _encode_bin(self, abstract_data): + def _encode_bin(self, abstract_data) -> bytes: chunks = [self.encode_record_bin(x) for x in abstract_data] # FIXME: pad to file size return b''.join(chunks) @@ -555,7 +795,12 @@ class RuntimeState(object): """Represent the runtime state of a session with a card.""" - def __init__(self, card, profile): + def __init__(self, card, profile:'CardProfile'): + """ + Args: + card : pysim.cards.Card instance + profile : CardProfile instance + """ self.mf = CardMF() self.card = card self.selected_file = self.mf @@ -589,15 +834,22 @@ print("error: could not determine card applications") return apps_taken - def get_cwd(self): - """Obtain the current working directory.""" + def get_cwd(self) -> CardDF: + """Obtain the current working directory. + + Returns: + CardDF instance + """ if isinstance(self.selected_file, CardDF): return self.selected_file else: return self.selected_file.parent - def get_application(self): - """Obtain the currently selected application (if any).""" + def get_application(self) -> Optional[CardADF]: + """Obtain the currently selected application (if any). + + Returns: + CardADF() instance or None""" # iterate upwards from selected file; check if any is an ADF node = self.selected_file while node.parent != node: @@ -606,9 +858,16 @@ node = node.parent return None - def interpret_sw(self, sw): - """Interpret the given SW relative to the currently selected Application - or the underlying profile.""" + def interpret_sw(self, sw:str): + """Interpret a given status word relative to the currently selected application + or the underlying card profile. + + Args: + sw : Status word as string of 4 hexd digits + + Returns: + Tuple of two strings + """ app = self.get_application() if app: # The application either comes with its own interpret_sw @@ -622,11 +881,9 @@ else: return self.profile.interpret_sw(sw) - def probe_file(self, fid, cmd_app=None): - """ - blindly try to select a file and automatically add a matching file - object if the file actually exists - """ + def probe_file(self, fid:str, cmd_app=None): + """Blindly try to select a file and automatically add a matching file + object if the file actually exists.""" if not is_hex(fid, 4, 4): raise ValueError("Cannot select unknown file by name %s, only hexadecimal 4 digit FID is allowed" % fid) @@ -651,8 +908,13 @@ self.selected_file = f return select_resp - def select(self, name, cmd_app=None): - """Change current directory""" + def select(self, name:str, cmd_app=None): + """Select a file (EF, DF, ADF, MF, ...). + + Args: + name : Name of file to select + cmd_app : Command Application State (for unregistering old file commands) + """ sels = self.selected_file.get_selectables() if is_hex(name): name = name.lower() @@ -686,43 +948,98 @@ return select_resp - def read_binary(self, length=None, offset=0): + def read_binary(self, length:int=None, offset:int=0): + """Read [part of] a transparent EF binary data. + + Args: + length : Amount of data to read (None: as much as possible) + offset : Offset into the file from which to read 'length' bytes + Returns: + binary data read from the file + """ if not isinstance(self.selected_file, TransparentEF): raise TypeError("Only works with TransparentEF") return self.card._scc.read_binary(self.selected_file.fid, length, offset) - def read_binary_dec(self): + def read_binary_dec(self) -> dict: + """Read [part of] a transparent EF binary data and decode it. + + Args: + length : Amount of data to read (None: as much as possible) + offset : Offset into the file from which to read 'length' bytes + Returns: + abstract decode data read from the file + """ (data, sw) = self.read_binary() dec_data = self.selected_file.decode_hex(data) print("%s: %s -> %s" % (sw, data, dec_data)) return (dec_data, sw) - def update_binary(self, data_hex, offset=0): + def update_binary(self, data_hex:str, offset:int=0): + """Update transparent EF binary data. + + Args: + data_hex : hex string of data to be written + offset : Offset into the file from which to write 'data_hex' + """ if not isinstance(self.selected_file, TransparentEF): raise TypeError("Only works with TransparentEF") return self.card._scc.update_binary(self.selected_file.fid, data_hex, offset, conserve=self.conserve_write) - def update_binary_dec(self, data): + def update_binary_dec(self, data:dict): + """Update transparent EF from abstract data. Encodes the data to binary and + then updates the EF with it. + + Args: + data : abstract data which is to be encoded and written + """ data_hex = self.selected_file.encode_hex(data) print("%s -> %s" % (data, data_hex)) return self.update_binary(data_hex) - def read_record(self, rec_nr=0): + def read_record(self, rec_nr:int=0): + """Read a record as binary data. + + Args: + rec_nr : Record number to read + Returns: + hex string of binary data contained in record + """ if not isinstance(self.selected_file, LinFixedEF): raise TypeError("Only works with Linear Fixed EF") # returns a string of hex nibbles return self.card._scc.read_record(self.selected_file.fid, rec_nr) - def read_record_dec(self, rec_nr=0): + def read_record_dec(self, rec_nr:int=0) -> Tuple[dict, str]: + """Read a record and decode it to abstract data. + + Args: + rec_nr : Record number to read + Returns: + abstract data contained in record + """ (data, sw) = self.read_record(rec_nr) return (self.selected_file.decode_record_hex(data), sw) - def update_record(self, rec_nr, data_hex): + def update_record(self, rec_nr:int, data_hex:str): + """Update a record with given binary data + + Args: + rec_nr : Record number to read + data_hex : Hex string binary data to be written + """ if not isinstance(self.selected_file, LinFixedEF): raise TypeError("Only works with Linear Fixed EF") return self.card._scc.update_record(self.selected_file.fid, rec_nr, data_hex, conserve=self.conserve_write) - def update_record_dec(self, rec_nr, data): + def update_record_dec(self, rec_nr:int, data:dict): + """Update a record with given abstract data. Will encode abstract to binary data + and then write it to the given record on the card. + + Args: + rec_nr : Record number to read + data_hex : Abstract data to be written + """ hex_data = self.selected_file.encode_record_hex(data) return self.update_record(self, rec_nr, data_hex) @@ -735,9 +1052,15 @@ self.fcp = None -def interpret_sw(sw_data, sw): - """Interpret a given status word within the profile. Returns tuple of - two strings""" +def interpret_sw(sw_data:dict, sw:str): + """Interpret a given status word. + + Args: + sw_data : Hierarchical dict of status word matches + sw : status word to match (string of 4 hex digits) + Returns: + tuple of two strings (class_string, description) + """ for class_str, swdict in sw_data.items(): # first try direct match if sw in swdict: @@ -751,7 +1074,12 @@ class CardApplication(object): """A card application is represented by an ADF (with contained hierarchy) and optionally some SW definitions.""" - def __init__(self, name, adf=None, sw=None): + def __init__(self, name, adf:str=None, sw:dict=None): + """ + Args: + adf : ADF name + sw : Dict of status word conversions + """ self.name = name self.adf = adf self.sw = sw or dict() @@ -760,8 +1088,14 @@ return "APP(%s)" % (self.name) def interpret_sw(self, sw): - """Interpret a given status word within the application. Returns tuple of - two strings""" + """Interpret a given status word within the application. + + Args: + sw : Status word as string of 4 hexd digits + + Returns: + Tuple of two strings + """ return interpret_sw(self.sw, sw) class CardProfile(object): @@ -769,6 +1103,14 @@ applications as well as profile-specific SW and shell commands. Every card has one card profile, but there may be multiple applications within that profile.""" def __init__(self, name, **kw): + """ + Args: + desc (str) : Description + files_in_mf : List of CardEF instances present in MF + applications : List of CardApplications present on card + sw : List of status word definitions + shell_cmdsets : List of cmd2 shell command sets of profile-specific commands + """ self.name = name self.desc = kw.get("desc", None) self.files_in_mf = kw.get("files_in_mf", []) @@ -779,10 +1121,21 @@ def __str__(self): return self.name - def add_application(self, app): + def add_application(self, app:CardApplication): + """Add an application to a card profile. + + Args: + app : CardApplication instance to be added to profile + """ self.applications.append(app) - def interpret_sw(self, sw): - """Interpret a given status word within the profile. Returns tuple of - two strings""" + def interpret_sw(self, sw:str): + """Interpret a given status word within the profile. + + Args: + sw : Status word as string of 4 hexd digits + + Returns: + Tuple of two strings + """ return interpret_sw(self.sw, sw) diff --git a/pySim/transport/__init__.py b/pySim/transport/__init__.py index d720259..f946af8 100644 --- a/pySim/transport/__init__.py +++ b/pySim/transport/__init__.py @@ -24,48 +24,53 @@ # class LinkBase(object): + """Base class for link/transport to card.""" - def wait_for_card(self, timeout=None, newcardonly=False): - """wait_for_card(): Wait for a card and connect to it + def wait_for_card(self, timeout:int=None, newcardonly:bool=False): + """Wait for a card and connect to it - timeout : Maximum wait time (None=no timeout) - newcardonly : Should we wait for a new card, or an already - inserted one ? + Args: + timeout : Maximum wait time in seconds (None=no timeout) + newcardonly : Should we wait for a new card, or an already inserted one ? """ pass def connect(self): - """connect(): Connect to a card immediately + """Connect to a card immediately """ pass def disconnect(self): - """disconnect(): Disconnect from card + """Disconnect from card """ pass def reset_card(self): - """reset_card(): Resets the card (power down/up) + """Resets the card (power down/up) """ pass - def send_apdu_raw(self, pdu): - """send_apdu_raw(pdu): Sends an APDU with minimal processing + def send_apdu_raw(self, pdu:str): + """Sends an APDU with minimal processing - pdu : string of hexadecimal characters (ex. "A0A40000023F00") - return : tuple(data, sw), where - data : string (in hex) of returned data (ex. "074F4EFFFF") - sw : string (in hex) of status word (ex. "9000") + Args: + pdu : string of hexadecimal characters (ex. "A0A40000023F00") + Returns: + tuple(data, sw), where + data : string (in hex) of returned data (ex. "074F4EFFFF") + sw : string (in hex) of status word (ex. "9000") """ pass def send_apdu(self, pdu): - """send_apdu(pdu): Sends an APDU and auto fetch response data + """Sends an APDU and auto fetch response data - pdu : string of hexadecimal characters (ex. "A0A40000023F00") - return : tuple(data, sw), where - data : string (in hex) of returned data (ex. "074F4EFFFF") - sw : string (in hex) of status word (ex. "9000") + Args: + pdu : string of hexadecimal characters (ex. "A0A40000023F00") + Returns: + tuple(data, sw), where + data : string (in hex) of returned data (ex. "074F4EFFFF") + sw : string (in hex) of status word (ex. "9000") """ data, sw = self.send_apdu_raw(pdu) @@ -82,15 +87,16 @@ return data, sw def send_apdu_checksw(self, pdu, sw="9000"): - """send_apdu_checksw(pdu,sw): Sends an APDU and check returned SW + """Sends an APDU and check returned SW - pdu : string of hexadecimal characters (ex. "A0A40000023F00") - sw : string of 4 hexadecimal characters (ex. "9000"). The - user may mask out certain digits using a '?' to add some - ambiguity if needed. - return : tuple(data, sw), where - data : string (in hex) of returned data (ex. "074F4EFFFF") - sw : string (in hex) of status word (ex. "9000") + Args: + pdu : string of hexadecimal characters (ex. "A0A40000023F00") + sw : string of 4 hexadecimal characters (ex. "9000"). The user may mask out certain + digits using a '?' to add some ambiguity if needed. + Returns: + tuple(data, sw), where + data : string (in hex) of returned data (ex. "074F4EFFFF") + sw : string (in hex) of status word (ex. "9000") """ rv = self.send_apdu(pdu) diff --git a/pySim/transport/calypso.py b/pySim/transport/calypso.py index 7f99d21..467d5ee 100644 --- a/pySim/transport/calypso.py +++ b/pySim/transport/calypso.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -""" pySim: Transport Link for Calypso bases phones -""" - -# # Copyright (C) 2018 Vadim Yanitskiy <axilirator at gmail.com> # # This program is free software: you can redistribute it and/or modify @@ -73,8 +69,9 @@ self.data += pdu class CalypsoSimLink(LinkBase): + """Transport Link for Calypso based phones.""" - def __init__(self, sock_path = "/tmp/osmocom_l2"): + def __init__(self, sock_path:str = "/tmp/osmocom_l2"): # Make sure that a given socket path exists if not os.path.exists(sock_path): raise ReaderError("There is no such ('%s') UNIX socket" % sock_path) @@ -119,7 +116,6 @@ pass # Nothing to do really ... def send_apdu_raw(self, pdu): - """see LinkBase.send_apdu_raw""" # Request FULL reset req_msg = L1CTLMessageSIM(h2b(pdu)) diff --git a/pySim/transport/modem_atcmd.py b/pySim/transport/modem_atcmd.py index 86d4443..fccd388 100644 --- a/pySim/transport/modem_atcmd.py +++ b/pySim/transport/modem_atcmd.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -""" pySim: Transport Link for 3GPP TS 27.007 compliant modems -""" - # Copyright (C) 2020 Vadim Yanitskiy <axilirator at gmail.com> # # This program is free software: you can redistribute it and/or modify @@ -31,7 +28,8 @@ # log.root.setLevel(log.DEBUG) class ModemATCommandLink(LinkBase): - def __init__(self, device='/dev/ttyUSB0', baudrate=115200): + """Transport Link for 3GPP TS 27.007 compliant modems.""" + def __init__(self, device:str='/dev/ttyUSB0', baudrate:int=115200): self._sl = serial.Serial(device, baudrate, timeout=5) self._device = device self._atr = None diff --git a/pySim/transport/pcsc.py b/pySim/transport/pcsc.py index 2c2cbb9..f08f71a 100644 --- a/pySim/transport/pcsc.py +++ b/pySim/transport/pcsc.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -""" pySim: PCSC reader transport link -""" - -# # Copyright (C) 2009-2010 Sylvain Munaut <tnt at 246tNt.com> # Copyright (C) 2010 Harald Welte <laforge at gnumonks.org> # @@ -32,8 +28,9 @@ class PcscSimLink(LinkBase): + """ pySim: PCSC reader transport link.""" - def __init__(self, reader_number=0): + def __init__(self, reader_number:int=0): r = readers() self._reader = r[reader_number] self._con = self._reader.createConnection() @@ -46,7 +43,7 @@ pass return - def wait_for_card(self, timeout=None, newcardonly=False): + def wait_for_card(self, timeout:int=None, newcardonly:bool=False): cr = CardRequest(readers=[self._reader], timeout=timeout, newcardonly=newcardonly) try: cr.waitforcard() @@ -75,7 +72,6 @@ return 1 def send_apdu_raw(self, pdu): - """see LinkBase.send_apdu_raw""" apdu = h2i(pdu) diff --git a/pySim/transport/serial.py b/pySim/transport/serial.py index 03d3f38..6d39303 100644 --- a/pySim/transport/serial.py +++ b/pySim/transport/serial.py @@ -1,9 +1,5 @@ # -*- coding: utf-8 -*- -""" pySim: Transport Link for serial (RS232) based readers included with simcard -""" - -# # Copyright (C) 2009-2010 Sylvain Munaut <tnt at 246tNt.com> # # This program is free software: you can redistribute it and/or modify @@ -30,8 +26,10 @@ class SerialSimLink(LinkBase): + """ pySim: Transport Link for serial (RS232) based readers included with simcard""" - def __init__(self, device='/dev/ttyUSB0', baudrate=9600, rst='-rts', debug=False): + def __init__(self, device:str='/dev/ttyUSB0', baudrate:int=9600, rst:str='-rts', + debug:bool=False): if not os.path.exists(device): raise ValueError("device file %s does not exist -- abort" % device) self._sl = serial.Serial( @@ -183,7 +181,6 @@ return self._sl.read() def send_apdu_raw(self, pdu): - """see LinkBase.send_apdu_raw""" pdu = h2b(pdu) data_len = ord(pdu[4]) # P3 diff --git a/pySim/utils.py b/pySim/utils.py index 0848b01..607526c 100644 --- a/pySim/utils.py +++ b/pySim/utils.py @@ -21,43 +21,65 @@ # -def h2b(s): +def h2b(s: str) -> bytearray: """convert from a string of hex nibbles to a sequence of bytes""" return bytearray.fromhex(s) -def b2h(b): +def b2h(b: bytearray) -> str: """convert from a sequence of bytes to a string of hex nibbles""" return ''.join(['%02x'%(x) for x in b]) -def h2i(s): +def h2i(s:str): + """convert from a string of hex nibbles to a list of integers""" return [(int(x,16)<<4)+int(y,16) for x,y in zip(s[0::2], s[1::2])] -def i2h(s): +def i2h(s) -> str: + """convert from a list of integers to a string of hex nibbles""" return ''.join(['%02x'%(x) for x in s]) -def h2s(s): +def h2s(s:str) -> str: + """convert from a string of hex nibbles to an ASCII string""" return ''.join([chr((int(x,16)<<4)+int(y,16)) for x,y in zip(s[0::2], s[1::2]) if int(x + y, 16) != 0xff]) -def s2h(s): +def s2h(s:str) -> str: + """convert from an ASCII string to a string of hex nibbles""" b = bytearray() b.extend(map(ord, s)) return b2h(b) # List of bytes to string -def i2s(s): +def i2s(s) -> str: + """convert from a list of integers to an ASCII string""" return ''.join([chr(x) for x in s]) -def swap_nibbles(s): +def swap_nibbles(s:str) -> str: + """swap the nibbles in a hex string""" return ''.join([x+y for x,y in zip(s[1::2], s[0::2])]) -def rpad(s, l, c='f'): +def rpad(s:str, l:int, c='f') -> str: + """pad string on the right side. + Args: + s : string to pad + l : total length to pad to + c : padding character + Returns: + String 's' padded with as many 'c' as needed to reach total length of 'l' + """ return s + c * (l - len(s)) -def lpad(s, l, c='f'): +def lpad(s:str, l:int, c='f') -> str: + """pad string on the left side. + Args: + s : string to pad + l : total length to pad to + c : padding character + Returns: + String 's' padded with as many 'c' as needed to reach total length of 'l' + """ return c * (l - len(s)) + s -def half_round_up(n): +def half_round_up(n:int) -> int: return (n + 1)//2 # IMSI encoded format: @@ -75,8 +97,8 @@ # Because of this, an odd length IMSI fits exactly into len(imsi) + 1 // 2 bytes, whereas an # even length IMSI only uses half of the last byte. -def enc_imsi(imsi): - """Converts a string imsi into the value of the EF""" +def enc_imsi(imsi:str): + """Converts a string IMSI into the encoded value of the EF""" l = half_round_up(len(imsi) + 1) # Required bytes - include space for odd/even indicator oe = len(imsi) & 1 # Odd (1) / Even (0) ei = '%02x' % l + swap_nibbles('%01x%s' % ((oe<<3)|1, rpad(imsi, 15))) @@ -781,7 +803,7 @@ return None -def sw_match(sw, pattern): +def sw_match(sw:str, pattern:str) -> str: """Match given SW against given pattern.""" # Create a masked version of the returned status word sw_lower = sw.lower() @@ -796,8 +818,18 @@ # Compare the masked version against the pattern return sw_masked == pattern -def tabulate_str_list(str_list, width = 79, hspace = 2, lspace = 1, align_left = True): - """Pretty print a list of strings into a tabulated form""" +def tabulate_str_list(str_list, width:int = 79, hspace:int = 2, lspace:int = 1, + align_left:bool = True): + """Pretty print a list of strings into a tabulated form. + + Args: + width : total width in characters per line + space : horizontal space between cells + lspace : number of spaces before row + align_lef : Align text to the left side + Returns: + multi-line string containing formatted table + """ if str_list == None: return "" if len(str_list) <= 0: -- To view, visit https://gerrit.osmocom.org/c/pysim/+/23577 To unsubscribe, or for help writing mail filters, visit https://gerrit.osmocom.org/settings Gerrit-Project: pysim Gerrit-Branch: master Gerrit-Change-Id: I6ac88e0662cf3c56ae32d86d50b18a8b4150571a Gerrit-Change-Number: 23577 Gerrit-PatchSet: 4 Gerrit-Owner: laforge <laforge at osmocom.org> Gerrit-Reviewer: Jenkins Builder Gerrit-Reviewer: dexter <pmaier at sysmocom.de> Gerrit-Reviewer: fixeria <vyanitskiy at sysmocom.de> Gerrit-Reviewer: herlesupreeth <herlesupreeth at gmail.com> Gerrit-Reviewer: laforge <laforge at osmocom.org> Gerrit-Reviewer: merlinchlosta <merlin.chlosta at rub.de> Gerrit-Reviewer: pespin <pespin at sysmocom.de> Gerrit-Reviewer: tnt <tnt at 246tNt.com> Gerrit-MessageType: merged -------------- next part -------------- An HTML attachment was scrubbed... URL: <http://lists.osmocom.org/pipermail/gerrit-log/attachments/20210402/2baf7307/attachment.htm>