add contrib/saip-tool.py
This is a tool to work with eSIM profiles in SAIP format. It allows to dump the contents, run constraint checkers as well as splitting of the PE-Sequence into the individual PEs. Change-Id: I396bcd594e0628dfc26bd90233317a77e2f91b20
This commit is contained in:
@@ -58,7 +58,8 @@ case "$JOB_TYPE" in
|
|||||||
--enable W0301 \
|
--enable W0301 \
|
||||||
pySim tests/*.py *.py \
|
pySim tests/*.py *.py \
|
||||||
contrib/es2p_client.py \
|
contrib/es2p_client.py \
|
||||||
contrib/es9p_client.py
|
contrib/es9p_client.py \
|
||||||
|
contrib/saip-tool.py
|
||||||
;;
|
;;
|
||||||
"docs")
|
"docs")
|
||||||
rm -rf docs/_build
|
rm -rf docs/_build
|
||||||
|
|||||||
157
contrib/saip-tool.py
Executable file
157
contrib/saip-tool.py
Executable file
@@ -0,0 +1,157 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
# (C) 2024 by Harald Welte <laforge@osmocom.org>
|
||||||
|
#
|
||||||
|
# This program is free software: you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Affero General Public License as published by
|
||||||
|
# the Free Software Foundation, either version 3 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 Affero General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import argparse
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from pySim.esim.saip import *
|
||||||
|
from pySim.esim.saip.validation import CheckBasicStructure
|
||||||
|
from pySim.utils import h2b, b2h, swap_nibbles
|
||||||
|
from pySim.pprint import HexBytesPrettyPrinter
|
||||||
|
|
||||||
|
pp = HexBytesPrettyPrinter(indent=4,width=500)
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
|
||||||
|
parser = argparse.ArgumentParser(description="""
|
||||||
|
Utility program to work with eSIM SAIP (SimAlliance Interoperable Profile) files.""")
|
||||||
|
parser.add_argument('INPUT_UPP', help='Unprotected Profile Package Input file')
|
||||||
|
subparsers = parser.add_subparsers(dest='command', help="The command to perform", required=True)
|
||||||
|
|
||||||
|
parser_split = subparsers.add_parser('split', help='Split PE-Sequence into individual PEs')
|
||||||
|
parser_split.add_argument('--output-prefix', default='.', help='Prefix path/filename for output files')
|
||||||
|
|
||||||
|
parser_dump = subparsers.add_parser('dump', help='Dump information on PE-Sequence')
|
||||||
|
parser_dump.add_argument('mode', choices=['all_pe', 'all_pe_by_type', 'all_pe_by_naa'])
|
||||||
|
parser_dump.add_argument('--dump-decoded', action='store_true', help='Dump decoded PEs')
|
||||||
|
|
||||||
|
parser_check = subparsers.add_parser('check', help='Run constraint checkers on PE-Sequence')
|
||||||
|
|
||||||
|
parser_rpe = subparsers.add_parser('remove-pe', help='Remove specified PEs from PE-Sequence')
|
||||||
|
parser_rpe.add_argument('--output-file', required=True, help='Output file name')
|
||||||
|
parser_rpe.add_argument('--identification', type=int, action='append', help='Remove PEs matching specified identification')
|
||||||
|
|
||||||
|
parser_rn = subparsers.add_parser('remove-naa', help='Remove speciifed NAAs from PE-Sequence')
|
||||||
|
parser_rn.add_argument('--output-file', required=True, help='Output file name')
|
||||||
|
parser_rn.add_argument('--naa-type', required=True, choices=NAAs.keys(), help='Network Access Application type to remove')
|
||||||
|
# TODO: add an --naa-index or the like, so only one given instance can be removed
|
||||||
|
|
||||||
|
|
||||||
|
def do_split(pes: ProfileElementSequence, opts):
|
||||||
|
i = 0
|
||||||
|
for pe in pes.pe_list:
|
||||||
|
basename = Path(opts.INPUT_UPP).stem
|
||||||
|
if not pe.identification:
|
||||||
|
fname = '%s-%02u-%s.der' % (basename, i, pe.type)
|
||||||
|
else:
|
||||||
|
fname = '%s-%02u-%05u-%s.der' % (basename, i, pe.identification, pe.type)
|
||||||
|
print("writing single PE to file '%s'" % fname)
|
||||||
|
with open(os.path.join(opts.output_prefix, fname), 'wb') as outf:
|
||||||
|
outf.write(pe.to_der())
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
def do_dump(pes: ProfileElementSequence, opts):
|
||||||
|
def print_all_pe(pes: ProfileElementSequence, dump_decoded:bool = False):
|
||||||
|
# iterate over each pe in the pes (using its __iter__ method)
|
||||||
|
for pe in pes:
|
||||||
|
print("="*70 + " " + pe.type)
|
||||||
|
if dump_decoded:
|
||||||
|
pp.pprint(pe.decoded)
|
||||||
|
|
||||||
|
def print_all_pe_by_type(pes: ProfileElementSequence, dump_decoded:bool = False):
|
||||||
|
# sort by PE type and show all PE within that type
|
||||||
|
for pe_type in pes.pe_by_type.keys():
|
||||||
|
print("="*70 + " " + pe_type)
|
||||||
|
for pe in pes.pe_by_type[pe_type]:
|
||||||
|
pp.pprint(pe)
|
||||||
|
if dump_decoded:
|
||||||
|
pp.pprint(pe.decoded)
|
||||||
|
|
||||||
|
def print_all_pe_by_naa(pes: ProfileElementSequence, dump_decoded:bool = False):
|
||||||
|
for naa in pes.pes_by_naa:
|
||||||
|
i = 0
|
||||||
|
for naa_instance in pes.pes_by_naa[naa]:
|
||||||
|
print("="*70 + " " + naa + str(i))
|
||||||
|
i += 1
|
||||||
|
for pe in naa_instance:
|
||||||
|
pp.pprint(pe.type)
|
||||||
|
if dump_decoded:
|
||||||
|
for d in pe.decoded:
|
||||||
|
print(" %s" % d)
|
||||||
|
|
||||||
|
if opts.mode == 'all_pe':
|
||||||
|
print_all_pe(pes, opts.dump_decoded)
|
||||||
|
elif opts.mode == 'all_pe_by_type':
|
||||||
|
print_all_pe_by_type(pes, opts.dump_decoded)
|
||||||
|
elif opts.mode == 'all_pe_by_naa':
|
||||||
|
print_all_pe_by_naa(pes, opts.dump_decoded)
|
||||||
|
|
||||||
|
def do_check(pes: ProfileElementSequence, opts):
|
||||||
|
print("Checking PE-Sequence structure...")
|
||||||
|
checker = CheckBasicStructure()
|
||||||
|
checker.check(pes)
|
||||||
|
print("All good!")
|
||||||
|
|
||||||
|
def do_remove_pe(pes: ProfileElementSequence, opts):
|
||||||
|
new_pe_list = []
|
||||||
|
for pe in pes.pe_list:
|
||||||
|
identification = pe.identification
|
||||||
|
if identification:
|
||||||
|
if identification in opts.identification:
|
||||||
|
print("Removing PE %s (id=%u) from Sequence..." % (pe, identification))
|
||||||
|
continue
|
||||||
|
new_pe_list.append(pe)
|
||||||
|
|
||||||
|
pes.pe_list = new_pe_list
|
||||||
|
pes._process_pelist()
|
||||||
|
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
|
||||||
|
with open(opts.output_file, 'wb') as f:
|
||||||
|
f.write(pes.to_der())
|
||||||
|
|
||||||
|
def do_remove_naa(pes: ProfileElementSequence, opts):
|
||||||
|
if not opts.naa_type in NAAs:
|
||||||
|
raise ValueError('unsupported NAA type %s' % opts.naa_type)
|
||||||
|
naa = NAAs[opts.naa_type]
|
||||||
|
print("Removing NAAs of type '%s' from Sequence..." % opts.naa_type)
|
||||||
|
pes.remove_naas_of_type(naa)
|
||||||
|
print("Writing %u PEs to file '%s'..." % (len(pes.pe_list), opts.output_file))
|
||||||
|
with open(opts.output_file, 'wb') as f:
|
||||||
|
f.write(pes.to_der())
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
opts = parser.parse_args()
|
||||||
|
|
||||||
|
with open(opts.INPUT_UPP, 'rb') as f:
|
||||||
|
pes = ProfileElementSequence.from_der(f.read())
|
||||||
|
|
||||||
|
print("Read %u PEs from file '%s'" % (len(pes.pe_list), opts.INPUT_UPP))
|
||||||
|
|
||||||
|
if opts.command == 'split':
|
||||||
|
do_split(pes, opts)
|
||||||
|
elif opts.command == 'dump':
|
||||||
|
do_dump(pes, opts)
|
||||||
|
elif opts.command == 'check':
|
||||||
|
do_check(pes, opts)
|
||||||
|
elif opts.command == 'remove-pe':
|
||||||
|
do_remove_pe(pes, opts)
|
||||||
|
elif opts.command == 'remove-naa':
|
||||||
|
do_remove_naa(pes, opts)
|
||||||
77
pySim/pprint.py
Normal file
77
pySim/pprint.py
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import pprint
|
||||||
|
from pprint import PrettyPrinter
|
||||||
|
from functools import singledispatch, wraps
|
||||||
|
from typing import get_type_hints
|
||||||
|
|
||||||
|
from pySim.utils import b2h
|
||||||
|
|
||||||
|
def common_container_checks(f):
|
||||||
|
type_ = get_type_hints(f)['object']
|
||||||
|
base_impl = type_.__repr__
|
||||||
|
empty_repr = repr(type_()) # {}, [], ()
|
||||||
|
too_deep_repr = f'{empty_repr[0]}...{empty_repr[-1]}' # {...}, [...], (...)
|
||||||
|
@wraps(f)
|
||||||
|
def wrapper(object, context, maxlevels, level):
|
||||||
|
if type(object).__repr__ is not base_impl: # subclassed repr
|
||||||
|
return repr(object)
|
||||||
|
if not object: # empty, short-circuit
|
||||||
|
return empty_repr
|
||||||
|
if maxlevels and level >= maxlevels: # exceeding the max depth
|
||||||
|
return too_deep_repr
|
||||||
|
oid = id(object)
|
||||||
|
if oid in context: # self-reference
|
||||||
|
return pprint._recursion(object)
|
||||||
|
context[oid] = 1
|
||||||
|
result = f(object, context, maxlevels, level)
|
||||||
|
del context[oid]
|
||||||
|
return result
|
||||||
|
return wrapper
|
||||||
|
|
||||||
|
@singledispatch
|
||||||
|
def saferepr(object, context, maxlevels, level):
|
||||||
|
return repr(object)
|
||||||
|
|
||||||
|
@saferepr.register
|
||||||
|
def _handle_bytes(object: bytes, *args):
|
||||||
|
if len(object) <= 40:
|
||||||
|
return '"%s"' % b2h(object)
|
||||||
|
else:
|
||||||
|
return '"%s...%s"' % (b2h(object[:20]), b2h(object[-20:]))
|
||||||
|
|
||||||
|
@saferepr.register
|
||||||
|
@common_container_checks
|
||||||
|
def _handle_dict(object: dict, context, maxlevels, level):
|
||||||
|
level += 1
|
||||||
|
contents = [
|
||||||
|
f'{saferepr(k, context, maxlevels, level)}: '
|
||||||
|
f'{saferepr(v, context, maxlevels, level)}'
|
||||||
|
for k, v in sorted(object.items(), key=pprint._safe_tuple)
|
||||||
|
]
|
||||||
|
return f'{{{", ".join(contents)}}}'
|
||||||
|
|
||||||
|
@saferepr.register
|
||||||
|
@common_container_checks
|
||||||
|
def _handle_list(object: list, context, maxlevels, level):
|
||||||
|
level += 1
|
||||||
|
contents = [
|
||||||
|
f'{saferepr(v, context, maxlevels, level)}'
|
||||||
|
for v in object
|
||||||
|
]
|
||||||
|
return f'[{", ".join(contents)}]'
|
||||||
|
|
||||||
|
@saferepr.register
|
||||||
|
@common_container_checks
|
||||||
|
def _handle_tuple(object: tuple, context, maxlevels, level):
|
||||||
|
level += 1
|
||||||
|
if len(object) == 1:
|
||||||
|
return f'({saferepr(object[0], context, maxlevels, level)},)'
|
||||||
|
contents = [
|
||||||
|
f'{saferepr(v, context, maxlevels, level)}'
|
||||||
|
for v in object
|
||||||
|
]
|
||||||
|
return f'({", ".join(contents)})'
|
||||||
|
|
||||||
|
class HexBytesPrettyPrinter(PrettyPrinter):
|
||||||
|
def format(self, *args):
|
||||||
|
# it doesn't matter what the boolean values are here
|
||||||
|
return saferepr(*args), True, False
|
||||||
Reference in New Issue
Block a user