v1.9.4: decode_step/hunt byte-replay tests + hunt EOF guard
- tests: TestDecodeStep drives decode_step() with a scripted byte queue (read/peek/consume stubbed over _replay + the queue; the re-align/PPS/ ATR/annotation sinks stubbed): 'CX'/'EX' exchanges frame and emit clean, 0x5F frames as a command (it was always eaten by the idle-noise gate before v1.9.3), the 0x61 text-byte boundary is a one-byte suspicious-CLA discard, the invalid-INS re-frame recovers a shifted SELECT, 0xFE still takes the idle-noise path. - tests: TestAtrHuntDetect drives handle_atr(): 0xC4 0xA4 / 0xC4 0x89 hand the header to DATA via _replay, CLA 0x00 stays excluded, an invalid INS does not hand off. - pd.py: the hunt command-detect guards peek_byte() returning None (a capture ending right after a plausible CLA byte raised TypeError, seen only by decode()'s crash protection); test_eof_after_plausible_cla_is_clean pins the clean return. - AGENTS v1.9.4 section; baseline regenerated (version + test listing; compare_baseline: IDENTICAL); 95/95 unit tests.
This commit is contained in:
1 parent
54dfc6586d
commit
883033e712
4 files changed
+271
-15
No files matched your search
@@ -333,6 +333,37 @@ Known limitation: no on-disk fixture reproduces the exact live failure (all
|
|||||||
real captures contain a 372 ATR), so end-to-end proof needs a re-captured
|
real captures contain a 372 ATR), so end-to-end proof needs a re-captured
|
||||||
`.sr` from the failing phone; the unit tests cover the loop-escape logic.
|
`.sr` from the failing phone; the unit tests cover the loop-escape logic.
|
||||||
|
|
||||||
|
### v1.9.4: decode_step/hunt unit coverage + hunt EOF guard
|
||||||
|
|
||||||
|
The v1.9.3 review's residual risk -- the new acceptance classes were only
|
||||||
|
proven by "no regression" on the example traces -- is closed with a
|
||||||
|
byte-replay harness, and the new coverage surfaced one latent defect:
|
||||||
|
|
||||||
|
- **`TestDecodeStep`** drives the real `decode_step()` DATA-state gate chain
|
||||||
|
with a scripted byte queue (`read_byte`/`peek_byte`/`_consume_first`
|
||||||
|
stubbed over `_replay` + the queue; the re-align/PPS/ATR/annotation sinks
|
||||||
|
stubbed; `reset()` provides the state). Pinned: `'CX'`/`'EX'` exchanges
|
||||||
|
frame and emit clean; `0x5F` (always eaten by the idle-noise gate before
|
||||||
|
v1.9.3) frames as a command; the `0x61` menu-text boundary byte is a
|
||||||
|
suspicious CLA with exactly one byte consumed; the invalid-INS one-byte
|
||||||
|
re-frame recovers a shifted SELECT into the intended packet; `0xFE` still
|
||||||
|
takes the idle-noise path.
|
||||||
|
- **`TestAtrHuntDetect`** drives `handle_atr()` the same way: `0xC4 0xA4`
|
||||||
|
and `0xC4 0x89` (the odd-INS acceptance) hand the header to DATA via
|
||||||
|
`_replay`; CLA `0x00` stays excluded; an invalid INS does not hand off.
|
||||||
|
- **Bug found and fixed:** the hunt's command-detect called
|
||||||
|
`plausible_ins(nxt)` with no None check -- a capture ending right after a
|
||||||
|
plausible CLA byte made `peek_byte()` return None and raised TypeError
|
||||||
|
(previously seen only by `decode()`'s crash protection).
|
||||||
|
`test_eof_after_plausible_cla_is_clean` pins the clean end-of-capture
|
||||||
|
return.
|
||||||
|
|
||||||
|
Validation: 95/95 unit tests; the new framing/hunt tests fail against
|
||||||
|
v1.9.2 for the right reasons (the acceptance is missing) and the EOF test
|
||||||
|
fails against v1.9.3 (the TypeError); full trace suite unchanged
|
||||||
|
(`compare_baseline.sh` IDENTICAL, baseline regenerated for the version +
|
||||||
|
test listing). VERSION -> 1.9.4.
|
||||||
|
|
||||||
### v1.9.3: higher logical channels in the CLA gate + defined odd INS codes
|
### v1.9.3: higher logical channels in the CLA gate + defined odd INS codes
|
||||||
|
|
||||||
A trace with higher logical channels (4-19) or an odd-INS command could not
|
A trace with higher logical channels (4-19) or an odd-INS command could not
|
||||||
@@ -852,7 +883,7 @@ append the measured CLK frequency when known
|
|||||||
|
|
||||||
### Versioning
|
### Versioning
|
||||||
|
|
||||||
The decoder version is defined in `pd.py` as `VERSION = '1.9.3'`.
|
The decoder version is defined in `pd.py` as `VERSION = '1.9.4'`.
|
||||||
The version is printed to the log on decoder startup.
|
The version is printed to the log on decoder startup.
|
||||||
|
|
||||||
### Testing after decoder changes
|
### Testing after decoder changes
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ from .gsmtap_stream import (GsmtapStreamSender,
|
|||||||
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
|
GSMTAP_SIM_RST_EVENT, GSMTAP_SIM_VCC_EVENT,
|
||||||
GSMTAP_FLAG_BAD_FCS)
|
GSMTAP_FLAG_BAD_FCS)
|
||||||
|
|
||||||
VERSION = '1.9.3'
|
VERSION = '1.9.4'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1766,7 +1766,10 @@ class Decoder(srd.Decoder):
|
|||||||
# on noise like the 0x00 0x1E seen in the live test.
|
# on noise like the 0x00 0x1E seen in the live test.
|
||||||
if plausible_cla(byte) and byte != 0x00:
|
if plausible_cla(byte) and byte != 0x00:
|
||||||
nxt = self.peek_byte()
|
nxt = self.peek_byte()
|
||||||
if plausible_ins(nxt):
|
# nxt is None at end-of-capture: keep hunting (the read
|
||||||
|
# below returns None and the hunt ends cleanly) instead of
|
||||||
|
# calling plausible_ins(None), which raises TypeError.
|
||||||
|
if nxt is not None and plausible_ins(nxt):
|
||||||
self._replay.appendleft(byte)
|
self._replay.appendleft(byte)
|
||||||
self.hasT0 = True
|
self.hasT0 = True
|
||||||
self.hasT1 = False
|
self.hasT1 = False
|
||||||
|
|||||||
+23
-12
@@ -1,7 +1,7 @@
|
|||||||
=== Baseline captured 2026-10-17T23:51:01+03:00 ===
|
=== Baseline captured 2026-10-18T02:13:20+03:00 ===
|
||||||
|
|
||||||
=== test_8 ===
|
=== test_8 ===
|
||||||
('=== iso7816 decoder v1.9.3 ===',)
|
('=== iso7816 decoder v1.9.4 ===',)
|
||||||
('etu recovered (samples):', 1237)
|
('etu recovered (samples):', 1237)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -36,7 +36,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== test_7 ===
|
=== test_7 ===
|
||||||
('=== iso7816 decoder v1.9.3 ===',)
|
('=== iso7816 decoder v1.9.4 ===',)
|
||||||
('etu recovered (samples):', 1236)
|
('etu recovered (samples):', 1236)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -77,7 +77,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== xiaomi_phone_sample ===
|
=== xiaomi_phone_sample ===
|
||||||
('=== iso7816 decoder v1.9.3 ===',)
|
('=== iso7816 decoder v1.9.4 ===',)
|
||||||
('etu recovered (samples):', 53)
|
('etu recovered (samples):', 53)
|
||||||
(11674007, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1])
|
(11674007, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1])
|
||||||
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 1, 1, 1, 1]
|
iso7816-1: CHKSUM ERROR bits=[0, 0, 0, 1, 0, 0, 1, 1, 1, 1]
|
||||||
@@ -102,7 +102,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== xiaomi_mi_a1_coldboot ===
|
=== xiaomi_mi_a1_coldboot ===
|
||||||
('=== iso7816 decoder v1.9.3 ===',)
|
('=== iso7816 decoder v1.9.4 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -129,7 +129,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== 4gmodem_coldboot ===
|
=== 4gmodem_coldboot ===
|
||||||
('=== iso7816 decoder v1.9.3 ===',)
|
('=== iso7816 decoder v1.9.4 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -156,7 +156,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== alcor_reader_sample ===
|
=== alcor_reader_sample ===
|
||||||
('=== iso7816 decoder v1.9.3 ===',)
|
('=== iso7816 decoder v1.9.4 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -189,7 +189,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_A55_coldboot_new_cable ===
|
=== samsung_A55_coldboot_new_cable ===
|
||||||
('=== iso7816 decoder v1.9.3 ===',)
|
('=== iso7816 decoder v1.9.4 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -216,7 +216,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_A55_coldboot_new_cable_sim2 ===
|
=== samsung_A55_coldboot_new_cable_sim2 ===
|
||||||
('=== iso7816 decoder v1.9.3 ===',)
|
('=== iso7816 decoder v1.9.4 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -243,7 +243,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_S21p_coldboot_new_cable ===
|
=== samsung_S21p_coldboot_new_cable ===
|
||||||
('=== iso7816 decoder v1.9.3 ===',)
|
('=== iso7816 decoder v1.9.4 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -270,7 +270,7 @@ Degradation metrics:
|
|||||||
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
RESULT: OK - capture fully explained (reader + CAT + valid retries)
|
||||||
|
|
||||||
=== samsung_S21p_coldboot_new_cable_sim2 ===
|
=== samsung_S21p_coldboot_new_cable_sim2 ===
|
||||||
('=== iso7816 decoder v1.9.3 ===',)
|
('=== iso7816 decoder v1.9.4 ===',)
|
||||||
('RST low at start: expecting cold-boot ATR',)
|
('RST low at start: expecting cold-boot ATR',)
|
||||||
iso7816-1: RST deasserted
|
iso7816-1: RST deasserted
|
||||||
('RST deasserted',)
|
('RST deasserted',)
|
||||||
@@ -342,6 +342,11 @@ test_explicit_eof_wins (__main__.TestAtEof.test_explicit_eof_wins) ... ok
|
|||||||
test_pending_replay_is_bounded (__main__.TestAtEof.test_pending_replay_is_bounded) ... ok
|
test_pending_replay_is_bounded (__main__.TestAtEof.test_pending_replay_is_bounded) ... ok
|
||||||
test_pending_replay_is_not_eof (__main__.TestAtEof.test_pending_replay_is_not_eof) ... ok
|
test_pending_replay_is_not_eof (__main__.TestAtEof.test_pending_replay_is_not_eof) ... ok
|
||||||
test_stuck_with_nothing_queued_is_eof (__main__.TestAtEof.test_stuck_with_nothing_queued_is_eof) ... ok
|
test_stuck_with_nothing_queued_is_eof (__main__.TestAtEof.test_stuck_with_nothing_queued_is_eof) ... ok
|
||||||
|
test_eof_after_plausible_cla_is_clean (__main__.TestAtrHuntDetect.test_eof_after_plausible_cla_is_clean) ... ok
|
||||||
|
test_handoff_on_extended_channel_cla (__main__.TestAtrHuntDetect.test_handoff_on_extended_channel_cla) ... ok
|
||||||
|
test_handoff_on_odd_ins_89 (__main__.TestAtrHuntDetect.test_handoff_on_odd_ins_89) ... ok
|
||||||
|
test_no_handoff_on_basic_channel_cla (__main__.TestAtrHuntDetect.test_no_handoff_on_basic_channel_cla) ... ok
|
||||||
|
test_no_handoff_on_invalid_ins (__main__.TestAtrHuntDetect.test_no_handoff_on_invalid_ins) ... ok
|
||||||
test_all_ff_idle_loop_is_bounded (__main__.TestAtrHuntEscape.test_all_ff_idle_loop_is_bounded) ... ok
|
test_all_ff_idle_loop_is_bounded (__main__.TestAtrHuntEscape.test_all_ff_idle_loop_is_bounded) ... ok
|
||||||
test_idle_ff_retries_before_cap (__main__.TestAtrHuntEscape.test_idle_ff_retries_before_cap) ... ok
|
test_idle_ff_retries_before_cap (__main__.TestAtrHuntEscape.test_idle_ff_retries_before_cap) ... ok
|
||||||
test_idle_resets_capped_then_resume (__main__.TestAtrHuntEscape.test_idle_resets_capped_then_resume) ... ok
|
test_idle_resets_capped_then_resume (__main__.TestAtrHuntEscape.test_idle_resets_capped_then_resume) ... ok
|
||||||
@@ -350,6 +355,12 @@ test_non_ff_resumes_when_count_high (__main__.TestAtrHuntEscape.test_non_ff_resu
|
|||||||
test_recover_etu_in_hunt_runs_measurement (__main__.TestAtrHuntEscape.test_recover_etu_in_hunt_runs_measurement) ... ok
|
test_recover_etu_in_hunt_runs_measurement (__main__.TestAtrHuntEscape.test_recover_etu_in_hunt_runs_measurement) ... ok
|
||||||
test_recovery_attempts_capped_then_hard_limit (__main__.TestAtrHuntEscape.test_recovery_attempts_capped_then_hard_limit) ... ok
|
test_recovery_attempts_capped_then_hard_limit (__main__.TestAtrHuntEscape.test_recovery_attempts_capped_then_hard_limit) ... ok
|
||||||
test_retry_then_recover_after_two_failures (__main__.TestAtrHuntEscape.test_retry_then_recover_after_two_failures) ... ok
|
test_retry_then_recover_after_two_failures (__main__.TestAtrHuntEscape.test_retry_then_recover_after_two_failures) ... ok
|
||||||
|
test_bit_dense_valid_cla_is_not_idle_noise (__main__.TestDecodeStep.test_bit_dense_valid_cla_is_not_idle_noise) ... ok
|
||||||
|
test_ex_class_exchange_frames (__main__.TestDecodeStep.test_ex_class_exchange_frames) ... ok
|
||||||
|
test_extended_channel_exchange_frames (__main__.TestDecodeStep.test_extended_channel_exchange_frames) ... ok
|
||||||
|
test_idle_noise_still_discarded (__main__.TestDecodeStep.test_idle_noise_still_discarded) ... ok
|
||||||
|
test_invalid_ins_re_frame_recovers_command (__main__.TestDecodeStep.test_invalid_ins_re_frame_recovers_command) ... ok
|
||||||
|
test_text_byte_boundary_is_discarded_not_framed (__main__.TestDecodeStep.test_text_byte_boundary_is_discarded_not_framed) ... ok
|
||||||
test_glitch_low_pulse_removed (__main__.TestDeglitchEdges.test_glitch_low_pulse_removed) ... ok
|
test_glitch_low_pulse_removed (__main__.TestDeglitchEdges.test_glitch_low_pulse_removed) ... ok
|
||||||
test_no_edges (__main__.TestDeglitchEdges.test_no_edges) ... ok
|
test_no_edges (__main__.TestDeglitchEdges.test_no_edges) ... ok
|
||||||
test_real_pulses_preserved (__main__.TestDeglitchEdges.test_real_pulses_preserved) ... ok
|
test_real_pulses_preserved (__main__.TestDeglitchEdges.test_real_pulses_preserved) ... ok
|
||||||
@@ -424,6 +435,6 @@ test_valid_ins_odd_variants (__main__.TestValidateT0Apdu.test_valid_ins_odd_vari
|
|||||||
test_vs_reader_mirror (__main__.TestValidateT0Apdu.test_vs_reader_mirror) ... ok
|
test_vs_reader_mirror (__main__.TestValidateT0Apdu.test_vs_reader_mirror) ... ok
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
Ran 84 tests in 0.025s
|
Ran 95 tests in 0.028s
|
||||||
|
|
||||||
OK
|
OK
|
||||||
@@ -752,5 +752,216 @@ class TestLineEventPayload(unittest.TestCase):
|
|||||||
self.assertEqual(self.pd.LINE_EVENT_FLAG_CLK_HZ, 0x01)
|
self.assertEqual(self.pd.LINE_EVENT_FLAG_CLK_HZ, 0x01)
|
||||||
|
|
||||||
|
|
||||||
|
class TestDecodeStep(unittest.TestCase):
|
||||||
|
"""Byte-replay harness for the DATA-state gate chain in decode_step().
|
||||||
|
|
||||||
|
The rest of the suite exercises only pure functions and leaf methods;
|
||||||
|
these tests drive the real state machine with a scripted byte queue so
|
||||||
|
the CLA/INS acceptance rules are pinned on the framing path (gate
|
||||||
|
ordering, header read, one-byte re-frame, exchange assembly), not just
|
||||||
|
in the predicates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
|
def _make(self, byte_seq):
|
||||||
|
inst = object.__new__(self.pd.Decoder)
|
||||||
|
inst.reset()
|
||||||
|
inst.samplenum = 0
|
||||||
|
inst._inverse_convention = False # normally set by start()
|
||||||
|
inst._atr_parsed = True # keep 0x3B on the DATA path
|
||||||
|
inst._primed = True # skip the first-sample RST/VCC priming
|
||||||
|
inst._at_eof = lambda: False
|
||||||
|
inst.state = 'DATA'
|
||||||
|
inst.hasT0 = True
|
||||||
|
inst.hasT1 = False
|
||||||
|
inst.bit_samples = 100 # ETU known: skip _measure_etu()
|
||||||
|
inst._pps_speed_changed = False
|
||||||
|
inst._resync = False
|
||||||
|
inst.out_ann = 0
|
||||||
|
inst.emitted = []
|
||||||
|
inst.discarded = []
|
||||||
|
inst._bytes = collections.deque(byte_seq)
|
||||||
|
|
||||||
|
def pop_byte():
|
||||||
|
if inst._replay:
|
||||||
|
return inst._replay.popleft()
|
||||||
|
return inst._bytes.popleft() if inst._bytes else None
|
||||||
|
|
||||||
|
def peek_byte():
|
||||||
|
b = inst._replay[0] if inst._replay else (
|
||||||
|
inst._bytes[0] if inst._bytes else None)
|
||||||
|
inst.peeked_samplenum = inst.samplenum
|
||||||
|
return b
|
||||||
|
|
||||||
|
inst.read_byte = pop_byte
|
||||||
|
inst.read_first_byte = pop_byte
|
||||||
|
inst._consume_first = pop_byte
|
||||||
|
inst.peek_byte = peek_byte
|
||||||
|
# The re-align / PPS / ATR / annotation paths are not under test.
|
||||||
|
inst.resync_idle = lambda: None
|
||||||
|
inst.handle_pps = lambda: None
|
||||||
|
inst.handle_atr = lambda pins: None
|
||||||
|
inst.put = lambda *a, **k: None
|
||||||
|
inst.log = lambda *a, **k: None
|
||||||
|
inst._note_discard = lambda value, reason, idle=False: \
|
||||||
|
inst.discarded.append((value, reason, idle))
|
||||||
|
inst._note_pre_sw = lambda value, reason: \
|
||||||
|
inst.discarded.append((value, reason, True))
|
||||||
|
inst.emit_packet = lambda sub_type, payload, ss, es, flags=0: \
|
||||||
|
inst.emitted.append((bytes(payload), flags))
|
||||||
|
# The vectors never depend on sample positions; a benign wait()
|
||||||
|
# keeps an unrelated wait call from crashing the harness.
|
||||||
|
inst.wait = lambda cond=None: [1] * 8
|
||||||
|
return inst
|
||||||
|
|
||||||
|
def test_extended_channel_exchange_frames(self):
|
||||||
|
# 'CX' STATUS on channel 4 (v1.9.3): the whole exchange must be
|
||||||
|
# read and emitted clean.
|
||||||
|
inst = self._make(bytes.fromhex('c0f20000009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted,
|
||||||
|
[(bytes.fromhex('c0f20000009000'), 0)])
|
||||||
|
self.assertEqual(inst.discarded, [])
|
||||||
|
|
||||||
|
def test_ex_class_exchange_frames(self):
|
||||||
|
# 'EX' STATUS on channel 6 with SM (b6 = 1): same.
|
||||||
|
inst = self._make(bytes.fromhex('e2f20000009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted,
|
||||||
|
[(bytes.fromhex('e2f20000009000'), 0)])
|
||||||
|
self.assertEqual(inst.discarded, [])
|
||||||
|
|
||||||
|
def test_bit_dense_valid_cla_is_not_idle_noise(self):
|
||||||
|
# 0x5F was accepted by plausible_cla() all along but always
|
||||||
|
# discarded by the idle-noise gate before v1.9.3 (dead code): the
|
||||||
|
# framing path must consume it as a command byte.
|
||||||
|
inst = self._make(bytes.fromhex('5fa40004009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted,
|
||||||
|
[(bytes.fromhex('5fa40004009000'), 0)])
|
||||||
|
self.assertEqual(inst.discarded, [])
|
||||||
|
|
||||||
|
def test_text_byte_boundary_is_discarded_not_framed(self):
|
||||||
|
# xiaomi_mi_a1_coldboot regression byte: the menu-text 'a' (0x61)
|
||||||
|
# at a mis-framed boundary is a suspicious CLA (not a frame start
|
||||||
|
# and not idle noise) -- exactly one byte is consumed.
|
||||||
|
inst = self._make(bytes.fromhex('612000c000621e'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted, [])
|
||||||
|
self.assertEqual(len(inst.discarded), 1)
|
||||||
|
self.assertEqual(inst.discarded[0][0], 0x61)
|
||||||
|
self.assertIn('suspicious CLA', inst.discarded[0][1])
|
||||||
|
self.assertEqual(bytes(inst._bytes), bytes.fromhex('2000c000621e'))
|
||||||
|
|
||||||
|
def test_invalid_ins_re_frame_recovers_command(self):
|
||||||
|
# A stray leading byte (0x1C) shifts the header; the invalid-INS
|
||||||
|
# window (INS 0x00) drops only that byte and re-frames the real
|
||||||
|
# command, which is emitted intact on the next step.
|
||||||
|
inst = self._make(bytes.fromhex('1c00a40004009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted, [])
|
||||||
|
self.assertEqual(inst.discarded[0][0], 0x1c)
|
||||||
|
self.assertIn('invalid INS', inst.discarded[0][1])
|
||||||
|
self.assertEqual(list(inst._replay), [0x00, 0xA4, 0x00, 0x04])
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted,
|
||||||
|
[(bytes.fromhex('00a40004009000'), 0)])
|
||||||
|
|
||||||
|
def test_idle_noise_still_discarded(self):
|
||||||
|
# 0xFE (all-1s minus one bit) is not a valid class: unchanged
|
||||||
|
# idle-noise discard.
|
||||||
|
inst = self._make(bytes.fromhex('fea40004009000'))
|
||||||
|
self.assertTrue(inst.decode_step())
|
||||||
|
self.assertEqual(inst.emitted, [])
|
||||||
|
self.assertEqual(len(inst.discarded), 1)
|
||||||
|
self.assertEqual(inst.discarded[0][0], 0xfe)
|
||||||
|
self.assertIn('idle-line noise', inst.discarded[0][1])
|
||||||
|
|
||||||
|
|
||||||
|
class TestAtrHuntDetect(unittest.TestCase):
|
||||||
|
"""The ATR-hunt command-detect handoff in handle_atr(), driven by a
|
||||||
|
scripted byte queue (the hunt logic itself is not otherwise covered)."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
|
def _make(self, byte_seq):
|
||||||
|
inst = object.__new__(self.pd.Decoder)
|
||||||
|
inst.reset()
|
||||||
|
inst.samplenum = 0
|
||||||
|
inst.ss = 0
|
||||||
|
inst._inverse_convention = False # normally set by start()
|
||||||
|
inst._samples_per_clock = 4.0 # skip _measure_clock_period()
|
||||||
|
inst.bit_samples = 100 # skip _compute_bit_samples()
|
||||||
|
inst.out_ann = 0
|
||||||
|
inst.hasT0 = False # start() defaults; a handoff flips it
|
||||||
|
inst.hasT1 = False
|
||||||
|
inst._bytes = collections.deque(byte_seq)
|
||||||
|
|
||||||
|
def pop_byte():
|
||||||
|
if inst._replay:
|
||||||
|
return inst._replay.popleft()
|
||||||
|
return inst._bytes.popleft() if inst._bytes else None
|
||||||
|
|
||||||
|
def peek_byte():
|
||||||
|
b = inst._replay[0] if inst._replay else (
|
||||||
|
inst._bytes[0] if inst._bytes else None)
|
||||||
|
inst.peeked_samplenum = inst.samplenum
|
||||||
|
return b
|
||||||
|
|
||||||
|
inst.read_byte = pop_byte
|
||||||
|
inst.read_first_byte = pop_byte
|
||||||
|
inst._consume_first = pop_byte
|
||||||
|
inst.peek_byte = peek_byte
|
||||||
|
inst.put = lambda *a, **k: None
|
||||||
|
inst.log = lambda *a, **k: None
|
||||||
|
inst.wait = lambda cond=None: [1] * 8
|
||||||
|
return inst
|
||||||
|
|
||||||
|
def test_handoff_on_extended_channel_cla(self):
|
||||||
|
# 0xC4 (channel 8, no SM) + 0xA4 (SELECT): a command header, not a
|
||||||
|
# non-TS byte; it is replayed to the DATA state.
|
||||||
|
inst = self._make([0xC4, 0xA4])
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertEqual(inst.state, 'DATA')
|
||||||
|
self.assertEqual(list(inst._replay), [0xC4])
|
||||||
|
self.assertTrue(inst.hasT0)
|
||||||
|
|
||||||
|
def test_handoff_on_odd_ins_89(self):
|
||||||
|
# The TS 102 221 AUTHENTICATE '89' form (v1.9.3): the second byte
|
||||||
|
# only qualifies because ODD_INS accepts it.
|
||||||
|
inst = self._make([0xC4, 0x89])
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertEqual(inst.state, 'DATA')
|
||||||
|
self.assertEqual(list(inst._replay), [0xC4])
|
||||||
|
|
||||||
|
def test_no_handoff_on_basic_channel_cla(self):
|
||||||
|
# CLA 0x00 is excluded by design (pre-activation noise).
|
||||||
|
inst = self._make([0x00, 0xA4])
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertEqual(inst.state, 'ATR')
|
||||||
|
self.assertEqual(list(inst._replay), [])
|
||||||
|
|
||||||
|
def test_no_handoff_on_invalid_ins(self):
|
||||||
|
# 0x61 is not a valid INS: the pair stays on the hunt path.
|
||||||
|
inst = self._make([0xC4, 0x61])
|
||||||
|
inst.handle_atr(None)
|
||||||
|
self.assertEqual(inst.state, 'ATR')
|
||||||
|
self.assertEqual(list(inst._replay), [])
|
||||||
|
|
||||||
|
def test_eof_after_plausible_cla_is_clean(self):
|
||||||
|
# Capture ends right after a plausible CLA byte: the hunt must
|
||||||
|
# return cleanly. It used to call plausible_ins(None) -> TypeError
|
||||||
|
# (seen only by decode()'s crash protection).
|
||||||
|
inst = self._make([0xC4])
|
||||||
|
inst.handle_atr(None) # must not raise
|
||||||
|
self.assertEqual(inst.state, 'ATR')
|
||||||
|
self.assertEqual(list(inst._replay), [])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
Reference in new issue
Block a user