fix: late-PoR handler on every path, one poll per packet, LOAD-step components (v3.6.37)

_send_envelope defined its SEND SHORT MESSAGE capture handler inside the
91xx branch, but the 'STATUS poll (PoR not captured)' path used it: a
packet whose ENVELOPE answered 9000 and whose late PoR arrived on the
STATUS poll raised UnboundLocalError, returned 500 and aborted the RAM
operation mid-packet with the card's command left pending (live
2026-09-29, 120-byte LOAD blocks).  The handler is now defined for every
path, and the late-PoR poll runs only after a packet's last segment
(poll_status from _send_secured_packet) - a concatenated download makes
no spurious mid-packet STATUS exchanges.

The PWA annotates every LOAD step with the CAP components its block
covers (ramStepComponents: 'completes Import' / 'inside Method' /
'Header...Import') and ramInstallFailHint names the component the failing
block completed - the live import-verification rejection lands in the
block completing Import (6438/6985 at both 240 B and 64 B blocks).

sw cache simple-v310; tests: 509 Python / 672 Node.
This commit is contained in:
2026-09-29 01:14:37 +03:00
parent f007d5c9be
commit ddf5cbb8e7
8 changed files with 194 additions and 32 deletions
+61
View File
@@ -304,6 +304,67 @@ class TestSmsConcatenation(unittest.TestCase):
else:
self.assertNotIn(concat + '7000', tpdu)
def test_send_secured_packet_polls_for_a_late_por_only_at_the_last_segment(self):
import pysim_simple_server.server as srv
apdu = '80E80000F0' + '00' * 240 + '00'
sp_hex, _ = _build_secured_packet('16', '01', '15', '15', 'b00000',
'0000000001', apdu, K, K)
polls = []
def fake_envelope(tpdu_hex, scc, sm_sc=None, submit_handler=None,
poll_status=True, **kwargs):
polls.append(poll_status)
return '', '9000'
with mock.patch.object(srv, '_send_envelope', side_effect=fake_envelope):
result = _send_secured_packet(object(), sp_hex, oa_number='12345')
self.assertTrue(result['success'], result)
self.assertGreater(result['segments'], 1)
self.assertEqual(polls, [False] * (result['segments'] - 1) + [True])
# a single-SMS packet keeps the immediate late-PoR poll
polls.clear()
with mock.patch.object(srv, '_send_envelope', side_effect=fake_envelope):
_send_secured_packet(object(), '00' * 10, oa_number='12345')
self.assertEqual(polls, [True])
def test_envelope_status_poll_carries_the_capture_handler(self):
# A late PoR (ENVELOPE 9000, then STATUS 91xx) must reach the
# proactive chain with the SEND SHORT MESSAGE capture handler. It
# used to be defined inside the 91xx branch only, so this path raised
# UnboundLocalError and killed the operation mid-packet with the
# command left pending (live 2026-09-29).
import pysim_simple_server.server as srv
class FakeTp:
def send_apdu(self, apdu):
return '', '9000'
class FakeScc:
cat_cla = '80'
_tp = FakeTp()
class FakeSubmit:
sms_segments = []
submit_tpdu_hex = None
submit_ud_hex = None
calls = []
with mock.patch.object(srv, '_send_status', return_value=('', '910b')), \
mock.patch.object(srv, '_handle_proactive_chain',
side_effect=lambda scc, sw, cb=None: calls.append((sw, cb))):
data, sw = srv._send_envelope('00', FakeScc(), submit_handler=FakeSubmit())
self.assertEqual(sw, '9000')
self.assertEqual(len(calls), 1)
self.assertEqual(calls[0][0], '910b')
self.assertTrue(callable(calls[0][1]))
# a mid-packet segment (poll_status=False) skips the poll entirely
with mock.patch.object(srv, '_send_status') as status, \
mock.patch.object(srv, '_handle_proactive_chain') as chain:
srv._send_envelope('00', FakeScc(), submit_handler=FakeSubmit(),
poll_status=False)
self.assertFalse(status.called)
self.assertFalse(chain.called)
def test_send_secured_packet_refuses_more_than_the_card_buffer(self):
length = SCP80_FIRST_BYTES + SCP80_NEXT_BYTES * (SCP80_MAX_SEGMENTS - 1) + 1
result = _send_secured_packet(object(), '00' * length, oa_number='12345')