v1.9.0: CLK rate in RST events; v1.9.1: T=0 NULL is not an SW1
v1.9.0 -- CLK frequency in RST events:
- RST events (sub_type 0x10) append the measured CLK frequency when known:
[direction, level, flags, clk_hz_be32], flags bit 0 = rate present; the
3-byte form is kept when the rate is not yet measured. Value is
samplerate / _samples_per_clock from the existing 16-edge measurement (PPS
changes F/D, not the CLK rate). VCC events unchanged. Backward
compatible (old consumers read the first two bytes only); consumers can
compute data_rate = clk_hz x D / F from the ATR/PPS Fi/Di.
- Unit tests for the payload codec; README/README_RUS/AGENTS updated.
v1.9.1 -- T=0 NULL is not an SW1:
- The 'scan for SW1' loops accepted any 0x6X byte, including the NULL
procedure byte 0x60 (card busy), so 'NULL + real SW1' was mis-framed as
the status word: the phone traces' SELECT MF came out as ...3f00 6061 —
real SW1 0x61 eaten as SW2, real SW2 0x29 left on the wire and discarded
as a suspicious CLA (desync cascade).
- plausible_sw() now accepts 0x61-0x6F and 0x90-0x9F only, per
ISO 7816-3:2006 10.3.3 Table 11 ('6X' or '9X', EXCEPT '60'; ISO 7816-4
enforces '60' as an invalid SW1) and UICC_SPECS.md 1.8. All seven
SW1-detection sites use plausible_sw(); NULLs met while scanning are
logged as rate-limited wait extensions (_note_pre_sw) instead of
suspicious bytes.
- tools/vs_reader.py::_plausible_sw() mirrors the fix; regression vector
for the 00a40004023f006061 artifact.
Validation: 74/74 unit tests; baseline regenerated and IDENTICAL (the SW
fix is count-neutral -- only the SW bytes change); all 9 example pcaps
regenerated -- vs_reader OK, zero APDUs ending in 60 61, SELECT MF now
61 29 / 61 25.
This commit is contained in:
1 parent
e5a2b7c83b
commit
250c09c9b7
7 files changed
+236
-58
No files matched your search
@@ -182,7 +182,11 @@ data), so the false-positive rate is very low. The checks are:
|
|||||||
`0x84`, `0xA0`, `0xB0`).
|
`0x84`, `0xA0`, `0xB0`).
|
||||||
3. INS byte is plausible: not `0x00`, not in `0x6x`/`0x9x` (status-word
|
3. INS byte is plausible: not `0x00`, not in `0x6x`/`0x9x` (status-word
|
||||||
collision), and the least-significant bit is `0`.
|
collision), and the least-significant bit is `0`.
|
||||||
4. The penultimate byte (SW1) is in `0x6x` or `0x9x`.
|
4. The penultimate byte (SW1) is in `0x61`–`0x6F` or `0x90`–`0x9F`.
|
||||||
|
`0x60` is explicitly invalid: it is the T=0 NULL procedure byte
|
||||||
|
(ISO 7816-3 §10.3.3 Table 11 marks SW1 as `'6X'` **except `'60'`**;
|
||||||
|
ISO 7816-4 enforces `'60'` as an invalid SW1), so a "NULL + real SW1"
|
||||||
|
pair must never be accepted as a status word.
|
||||||
|
|
||||||
The current validation deliberately does **not** enforce P3-based length
|
The current validation deliberately does **not** enforce P3-based length
|
||||||
constraints, because a card may reject a command and send the status word
|
constraints, because a card may reject a command and send the status word
|
||||||
@@ -304,6 +308,58 @@ 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.1: T=0 NULL is not an SW1 (fixes bogus "60 61" APDU endings)
|
||||||
|
|
||||||
|
The "scan for SW1" loops accepted any byte with high nibble `0x6` as a
|
||||||
|
status word, including the T=0 **NULL procedure byte `0x60`** (card busy /
|
||||||
|
waiting-time extension). A card that holds the terminal with NULLs while
|
||||||
|
busy was therefore mis-framed: on the phone/cold-boot traces the SELECT MF
|
||||||
|
wire flow is `00 a4 00 04 02` → ACK `a4` → command data `3f 00` → ~1.4 s of
|
||||||
|
NULL `60` → real SW `61 29`. The decoder took `60` as SW1 and the real
|
||||||
|
`0x61` as SW2, emitting `…3f00 6061` and leaving the real SW2 (`0x29`) on
|
||||||
|
the wire, where it was discarded as a suspicious CLA — and the bogus packet
|
||||||
|
even passed `validate_t0_apdu()` because `plausible_sw()` accepted `0x60`.
|
||||||
|
|
||||||
|
- `plausible_sw()` now accepts `0x61`–`0x6F` and `0x90`–`0x9F` only.
|
||||||
|
Authority: ISO 7816-3:2006 §10.3.3 Table 11 — "If the value is `'6X'`
|
||||||
|
or `'9X'`, **except for `'60'`**, it is a SW1 byte"; the note adds that
|
||||||
|
ISO 7816-4 enforces `'60'` as an invalid SW1. (UICC_SPECS.md §1.8
|
||||||
|
classes: normal `'9000'`/`'61XX'`; warning `'62XX'`/`'63XX'`; execution
|
||||||
|
error `'64XX'`–`'66XX'`; checking error `'67XX'`–`'6FXX'`.)
|
||||||
|
- Every SW1-detection site now uses `plausible_sw()`: the edge ACK scan,
|
||||||
|
the P3=0 direct-SW check and response scan, the case-4 response scan,
|
||||||
|
the case-3 SW1 check, the unexpected-byte scan, and the ATR path.
|
||||||
|
- NULLs met while scanning for the status word are logged as rate-limited
|
||||||
|
wait extensions via the new `_note_pre_sw()` (cards emit them in bursts,
|
||||||
|
so they must not flood the log or raise warnings); other non-SW bytes
|
||||||
|
keep their call-site reason.
|
||||||
|
- `tools/vs_reader.py::_plausible_sw()` mirrors the fix.
|
||||||
|
- Tests: `plausible_sw` vectors updated (`0x60` now rejected; accepts
|
||||||
|
start at `0x61`), plus a regression vector for the
|
||||||
|
`00 a4 00 04 02 3f 00 60 61` artifact (invalid SW1 / desynced).
|
||||||
|
|
||||||
|
The fix is count-neutral (the same bytes are consumed; only the SW bytes
|
||||||
|
change), so `tests/baseline.txt` differs only by the version line and the
|
||||||
|
new test listing. 74/74 unit tests. VERSION -> 1.9.1.
|
||||||
|
|
||||||
|
### v1.9.0: CLK frequency in RST events
|
||||||
|
|
||||||
|
RST line events (sub_type 0x10) now append the measured CLK frequency when
|
||||||
|
it is known: `[direction, level, flags, clk_hz_be32]`, with `flags` bit 0
|
||||||
|
(`LINE_EVENT_FLAG_CLK_HZ`) set and the frequency as a big-endian uint32 in
|
||||||
|
Hz (7-byte payload). An unknown rate keeps the 3-byte payload, so
|
||||||
|
consumers that read only the first two bytes (e.g. pysniff's
|
||||||
|
`decode_line_event`) are unaffected. The value is the existing 16-edge
|
||||||
|
average (`samplerate / _samples_per_clock`, see `_measure_clock_period`);
|
||||||
|
PPS changes F/D, not the CLK rate, so one measurement per session is
|
||||||
|
enough. VCC events (0x11) are unchanged. Purpose: consumers can compute
|
||||||
|
the actual data rate as `clk_hz × D / F` from the Fi/Di negotiated in the
|
||||||
|
ATR/PPS. Ordering: the RST deassert event usually precedes the ATR event
|
||||||
|
(the 10 ms hysteresis lands mid-ATR for full ATRs), but a short ATR can
|
||||||
|
invert that, so consumers take the rate from any RST event. Wire
|
||||||
|
extension is backward compatible; 73/73 unit tests; baseline regenerated
|
||||||
|
and IDENTICAL (it tracks no RST payload metrics). VERSION -> 1.9.0.
|
||||||
|
|
||||||
### v1.8.0: PPS emitted as standard request + response packets
|
### v1.8.0: PPS emitted as standard request + response packets
|
||||||
|
|
||||||
The PPS exchange was previously emitted as a single packet under sub_type
|
The PPS exchange was previously emitted as a single packet under sub_type
|
||||||
@@ -559,9 +615,9 @@ window may be filtering real start bits.
|
|||||||
|------|---------|
|
|------|---------|
|
||||||
| ACK (== INS or == ~INS) | Full data match: card acknowledges and expects all P3 data bytes |
|
| ACK (== INS or == ~INS) | Full data match: card acknowledges and expects all P3 data bytes |
|
||||||
| INS^0x01 or ~INS^0x01 | Single-byte match: card validates syntax but processes data one byte at a time |
|
| INS^0x01 or ~INS^0x01 | Single-byte match: card validates syntax but processes data one byte at a time |
|
||||||
| 0x60 | NULL: card busy, wait for next procedure byte |
|
| 0x60 | NULL: card busy, wait for next procedure byte (never an SW1) |
|
||||||
| 0x9E / 0x9F | Extended-length: next byte L = number of response data bytes, then SW |
|
| 0x9E / 0x9F | Extended-length: next byte L = number of response data bytes, then SW |
|
||||||
| SW1 (0x6x / 0x9x) | Status word: exchange complete |
|
| SW1 (`0x61`–`0x6F` / `0x9X`) | Status word: exchange complete |
|
||||||
|
|
||||||
##### CLA (Class) Byte — Interindustry Structure
|
##### CLA (Class) Byte — Interindustry Structure
|
||||||
|
|
||||||
@@ -642,11 +698,13 @@ comments in `gsmtap_stream.py`). It deviates in two documented ways:
|
|||||||
`GSMTAP_FLAG_BAD_FCS` writes flags into the header `res` byte, which
|
`GSMTAP_FLAG_BAD_FCS` writes flags into the header `res` byte, which
|
||||||
official gsmtap.h reserves. The `res`-byte carriage is this decoder's own
|
official gsmtap.h reserves. The `res`-byte carriage is this decoder's own
|
||||||
extension (upstream simtrace2 never sets `res`); the semantics mirror
|
extension (upstream simtrace2 never sets `res`); the semantics mirror
|
||||||
simtrace2's per-message USB data flags.
|
simtrace2's per-message USB data flags. 0x10 RST events additionally
|
||||||
|
append the measured CLK frequency when known
|
||||||
|
(`[direction, level, flags, clk_hz_be32]`; see "v1.9.0" below).
|
||||||
|
|
||||||
### Versioning
|
### Versioning
|
||||||
|
|
||||||
The decoder version is defined in `pd.py` as `VERSION = '1.8.0'`.
|
The decoder version is defined in `pd.py` as `VERSION = '1.9.1'`.
|
||||||
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
|
||||||
|
|||||||
@@ -83,7 +83,7 @@ GSMTAP header: version 2, type SIM (0x04), UDP port 4729.
|
|||||||
| 0x01 | ATR | raw ATR bytes |
|
| 0x01 | ATR | raw ATR bytes |
|
||||||
| 0x02 | PPS request | raw PPS request bytes |
|
| 0x02 | PPS request | raw PPS request bytes |
|
||||||
| 0x03 | PPS response | raw PPS response bytes |
|
| 0x03 | PPS response | raw PPS response bytes |
|
||||||
| 0x10 | RST event (custom) | `[direction, level, 0x00]` |
|
| 0x10 | RST event (custom) | `[direction, level, flags, clk_hz_be32]` — CLK frequency appended when known |
|
||||||
| 0x11 | VCC event (custom) | `[direction, level, 0x00]` |
|
| 0x11 | VCC event (custom) | `[direction, level, 0x00]` |
|
||||||
|
|
||||||
Event payload byte semantics:
|
Event payload byte semantics:
|
||||||
@@ -96,6 +96,13 @@ Event payload byte semantics:
|
|||||||
(PPS request and response are emitted as separate packets). 0x10/0x11
|
(PPS request and response are emitted as separate packets). 0x10/0x11
|
||||||
are custom extensions not defined in libosmocore's gsmtap.h.
|
are custom extensions not defined in libosmocore's gsmtap.h.
|
||||||
|
|
||||||
|
RST events carry the measured CLK frequency when it is known: `flags`
|
||||||
|
bit 0 is then set and the frequency follows as a big-endian uint32 in Hz
|
||||||
|
(7-byte payload). When the rate is not yet measured the payload stays
|
||||||
|
3 bytes, so consumers that read only the first two bytes are unaffected.
|
||||||
|
Combined with the F/D negotiated in the ATR/PPS, the data rate is
|
||||||
|
`clk_hz × D / F` bits/s.
|
||||||
|
|
||||||
Header extensions: flags such as `GSMTAP_FLAG_BAD_FCS` are written into
|
Header extensions: flags such as `GSMTAP_FLAG_BAD_FCS` are written into
|
||||||
the header `res` byte, which official gsmtap.h marks reserved (RFU).
|
the header `res` byte, which official gsmtap.h marks reserved (RFU).
|
||||||
This carriage is this decoder's own extension (upstream simtrace2-sniff
|
This carriage is this decoder's own extension (upstream simtrace2-sniff
|
||||||
|
|||||||
+8
-1
@@ -86,7 +86,7 @@ GSMTAP pcap-файлы (включая PCAP, который этот декод
|
|||||||
| 0x01 | ATR | сырые байты ATR |
|
| 0x01 | ATR | сырые байты ATR |
|
||||||
| 0x02 | Запрос PPS | сырые байты запроса PPS |
|
| 0x02 | Запрос PPS | сырые байты запроса PPS |
|
||||||
| 0x03 | Ответ PPS | сырые байты ответа PPS |
|
| 0x03 | Ответ PPS | сырые байты ответа PPS |
|
||||||
| 0x10 | Событие RST (кастомное) | `[direction, level, 0x00]` |
|
| 0x10 | Событие RST (кастомное) | `[direction, level, flags, clk_hz_be32]` — частота CLK добавляется, когда известна |
|
||||||
| 0x11 | Событие VCC (кастомное) | `[direction, level, 0x00]` |
|
| 0x11 | Событие VCC (кастомное) | `[direction, level, 0x00]` |
|
||||||
|
|
||||||
Семантика байтов полезной нагрузки:
|
Семантика байтов полезной нагрузки:
|
||||||
@@ -100,6 +100,13 @@ GSMTAP pcap-файлы (включая PCAP, который этот декод
|
|||||||
0x10/0x11 — кастомные расширения, не определённые в gsmtap.h из
|
0x10/0x11 — кастомные расширения, не определённые в gsmtap.h из
|
||||||
libosmocore.
|
libosmocore.
|
||||||
|
|
||||||
|
События RST несут измеренную частоту CLK, когда она известна: тогда
|
||||||
|
устанавливается бит 0 байта `flags`, а следом идёт частота как big-endian
|
||||||
|
uint32 в герцах (полезная нагрузка 7 байт). Пока частота не измерена,
|
||||||
|
нагрузка остаётся 3 байта, поэтому потребители, читающие только первые два
|
||||||
|
байта, не затрагиваются. Вместе с F/D, согласованными в ATR/PPS, скорость
|
||||||
|
передачи данных равна `clk_hz × D / F` бит/с.
|
||||||
|
|
||||||
Расширения заголовка: флаги вроде `GSMTAP_FLAG_BAD_FCS` пишутся в байт
|
Расширения заголовка: флаги вроде `GSMTAP_FLAG_BAD_FCS` пишутся в байт
|
||||||
`res` заголовка, который официальный gsmtap.h помечает как зарезервированный
|
`res` заголовка, который официальный gsmtap.h помечает как зарезервированный
|
||||||
(RFU). Это собственное расширение данного декодера (upstream simtrace2-sniff
|
(RFU). Это собственное расширение данного декодера (upstream simtrace2-sniff
|
||||||
|
|||||||
@@ -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.8.0'
|
VERSION = '1.9.1'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -106,10 +106,17 @@ class pcap_udp_pkt():
|
|||||||
return len(self.h) + len(self.data)
|
return len(self.h) + len(self.data)
|
||||||
|
|
||||||
def plausible_sw(sw1):
|
def plausible_sw(sw1):
|
||||||
'''T=0 status word first byte sanity check. SW1 is 0x9x (normal
|
'''T=0 status word first byte sanity check.
|
||||||
processing) or 0x6x (warning/error classes). Anything else means
|
|
||||||
the byte stream was almost certainly mis-framed.'''
|
Per ISO 7816-3:2006 §10.3.3 Table 11, an SW1 byte is '6X' or '9X'
|
||||||
return (0x60 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
EXCEPT '60': that value is the NULL procedure byte (card busy /
|
||||||
|
waiting-time extension), and ISO 7816-4 enforces '60' as an invalid
|
||||||
|
SW1. Accepting it mis-frames "NULL + real SW1" as the status word --
|
||||||
|
the bogus "60 61" APDU ending, where the real SW1 (0x61) is eaten as
|
||||||
|
SW2. The 6X classes are '61'..'6F' (normal '61XX'; warning '62XX'/
|
||||||
|
'63XX'; execution error '64XX'-'66XX'; checking error '67XX'-'6FXX'),
|
||||||
|
plus '9X' for normal processing.'''
|
||||||
|
return (0x61 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
||||||
|
|
||||||
|
|
||||||
def plausible_cla(cla):
|
def plausible_cla(cla):
|
||||||
@@ -272,6 +279,29 @@ def embedded_exchanges(buf):
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
# --- line-event payload extension (RST/VCC, sub_type 0x10/0x11) ---
|
||||||
|
# The base payload is [direction, level, flags]; byte 2 used to be reserved
|
||||||
|
# and always 0. Bit 0 of it now marks an appended CLK frequency (big-endian
|
||||||
|
# uint32 Hz), so consumers that only read the first two bytes keep working.
|
||||||
|
LINE_EVENT_FLAG_CLK_HZ = 0x01
|
||||||
|
|
||||||
|
|
||||||
|
def line_event_payload(direction, level, clk_hz=None):
|
||||||
|
'''Build a RST/VCC line-event payload (GSMTAP sub_type 0x10/0x11).
|
||||||
|
|
||||||
|
Base form: [direction, level, flags] (3 bytes). When `clk_hz` is
|
||||||
|
known, flags bit 0 is set and the frequency follows as a big-endian
|
||||||
|
uint32 in Hz, giving a 7-byte payload. Old consumers read only the
|
||||||
|
first two bytes and ignore the rest, so the extension is backward
|
||||||
|
compatible.'''
|
||||||
|
flags = 0
|
||||||
|
extra = b''
|
||||||
|
if clk_hz:
|
||||||
|
flags |= LINE_EVENT_FLAG_CLK_HZ
|
||||||
|
extra = struct.pack('>I', int(clk_hz))
|
||||||
|
return bytes([direction & 0xFF, level & 0xFF, flags]) + extra
|
||||||
|
|
||||||
|
|
||||||
MAX_TPDU_LEN = 271 # ISO 7816-3 T=0: header(5) + proc + 255 + SW(2) < 271
|
MAX_TPDU_LEN = 271 # ISO 7816-3 T=0: header(5) + proc + 255 + SW(2) < 271
|
||||||
IDLE_RESYNC_ETU = 4 # idle bit-periods marking an exchange boundary
|
IDLE_RESYNC_ETU = 4 # idle bit-periods marking an exchange boundary
|
||||||
MAX_RESYNC_ATTEMPTS = 1024 # bail out if no idle gap found in that many tries
|
MAX_RESYNC_ATTEMPTS = 1024 # bail out if no idle gap found in that many tries
|
||||||
@@ -405,6 +435,20 @@ class Decoder(srd.Decoder):
|
|||||||
[Ann.ANN_WARN, ["dropped 0x%02x: %s" % (value, reason)]])
|
[Ann.ANN_WARN, ["dropped 0x%02x: %s" % (value, reason)]])
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def _note_pre_sw(self, value, reason):
|
||||||
|
'''Log a byte consumed while scanning for the status word.
|
||||||
|
|
||||||
|
A T=0 NULL (0x60) is a legitimate wait extension -- cards emit them
|
||||||
|
in bursts while busy -- so it is reported like idle noise
|
||||||
|
(rate-limited) with its own reason instead of raising a warning.
|
||||||
|
Other non-SW bytes keep the call-site reason.'''
|
||||||
|
if value == 0x60:
|
||||||
|
self._note_discard(value,
|
||||||
|
'NULL procedure byte (card busy) before '
|
||||||
|
'status word', idle=True)
|
||||||
|
else:
|
||||||
|
self._note_discard(value, reason, idle=(value in (0x00, 0xFF)))
|
||||||
|
|
||||||
def _consume_first(self):
|
def _consume_first(self):
|
||||||
'''Consume the byte the last peek_byte() returned, removing it from
|
'''Consume the byte the last peek_byte() returned, removing it from
|
||||||
whichever source it came from (the _replay buffer or the peeked
|
whichever source it came from (the _replay buffer or the peeked
|
||||||
@@ -668,6 +712,21 @@ class Decoder(srd.Decoder):
|
|||||||
return (cand, cand_sn, cand, lvl, now)
|
return (cand, cand_sn, cand, lvl, now)
|
||||||
return (None, 0, confirmed, lvl, now)
|
return (None, 0, confirmed, lvl, now)
|
||||||
|
|
||||||
|
def _clk_hz(self):
|
||||||
|
'''Return the measured CLK frequency in Hz, or None when unknown.
|
||||||
|
|
||||||
|
Uses the 16-edge average from _measure_clock_period(). The value is
|
||||||
|
a session constant: PPS changes F/D (CLK cycles per bit), not the
|
||||||
|
physical CLK rate. Native mode only (the measurement is skipped in
|
||||||
|
sample_as_clock / detect modes).'''
|
||||||
|
spc = self._samples_per_clock
|
||||||
|
sr = self.samplerate
|
||||||
|
if spc and sr:
|
||||||
|
hz = int(round(float(sr) / float(spc)))
|
||||||
|
if 1 <= hz <= 100000000:
|
||||||
|
return hz
|
||||||
|
return None
|
||||||
|
|
||||||
def line_events(self, pins):
|
def line_events(self, pins):
|
||||||
'''Check RST/VCC levels via level-stability hysteresis and emit
|
'''Check RST/VCC levels via level-stability hysteresis and emit
|
||||||
events + annotations for confirmed level changes.
|
events + annotations for confirmed level changes.
|
||||||
@@ -684,7 +743,9 @@ class Decoder(srd.Decoder):
|
|||||||
if emit_lvl is not None:
|
if emit_lvl is not None:
|
||||||
direction = 1 if emit_lvl == 0 else 0 # 1=asserted, 0=deasserted
|
direction = 1 if emit_lvl == 0 else 0 # 1=asserted, 0=deasserted
|
||||||
self.emit_packet(GSMTAP_SIM_RST_EVENT,
|
self.emit_packet(GSMTAP_SIM_RST_EVENT,
|
||||||
bytes([direction, emit_lvl, 0]), edge_sn, edge_sn)
|
line_event_payload(direction, emit_lvl,
|
||||||
|
self._clk_hz()),
|
||||||
|
edge_sn, edge_sn)
|
||||||
text = ('RST asserted' if emit_lvl == 0 else 'RST deasserted')
|
text = ('RST asserted' if emit_lvl == 0 else 'RST deasserted')
|
||||||
self.put(edge_sn, edge_sn, self.out_ann, [Ann.ANN_RST, [text]])
|
self.put(edge_sn, edge_sn, self.out_ann, [Ann.ANN_RST, [text]])
|
||||||
self.log(text)
|
self.log(text)
|
||||||
@@ -2319,13 +2380,11 @@ class Decoder(srd.Decoder):
|
|||||||
sw1 = self.read_byte()
|
sw1 = self.read_byte()
|
||||||
if sw1 is None:
|
if sw1 is None:
|
||||||
return True
|
return True
|
||||||
if ((sw1 & 0xF0) == 0x60
|
if plausible_sw(sw1):
|
||||||
or (sw1 & 0xF0) == 0x90):
|
|
||||||
break
|
break
|
||||||
self._note_discard(sw1,
|
self._note_pre_sw(
|
||||||
'non-SW byte before status word '
|
sw1, 'non-SW byte before status word '
|
||||||
'(P3-bounded response)',
|
'(P3-bounded response)')
|
||||||
idle=(sw1 in (0x00, 0xFF)))
|
|
||||||
if sw1 is None:
|
if sw1 is None:
|
||||||
return True
|
return True
|
||||||
sw2 = self.read_byte()
|
sw2 = self.read_byte()
|
||||||
@@ -2341,8 +2400,7 @@ class Decoder(srd.Decoder):
|
|||||||
if (p3 == 0):
|
if (p3 == 0):
|
||||||
# P3=0 (Le=0): no command data. Card sends
|
# P3=0 (Le=0): no command data. Card sends
|
||||||
# response data + SW, or SW directly (no ACK).
|
# response data + SW, or SW directly (no ACK).
|
||||||
if ((pb0 & 0xF0) == 0x60
|
if plausible_sw(pb0):
|
||||||
or (pb0 & 0xF0) == 0x90):
|
|
||||||
# pb0 is SW1: card sent SW directly.
|
# pb0 is SW1: card sent SW directly.
|
||||||
sw2 = self.read_byte()
|
sw2 = self.read_byte()
|
||||||
if sw2 is None:
|
if sw2 is None:
|
||||||
@@ -2356,8 +2414,7 @@ class Decoder(srd.Decoder):
|
|||||||
b = self.read_byte()
|
b = self.read_byte()
|
||||||
if b is None:
|
if b is None:
|
||||||
return True
|
return True
|
||||||
if ((b & 0xF0) == 0x60
|
if plausible_sw(b):
|
||||||
or (b & 0xF0) == 0x90):
|
|
||||||
sw2 = self.read_byte()
|
sw2 = self.read_byte()
|
||||||
if sw2 is None:
|
if sw2 is None:
|
||||||
return True
|
return True
|
||||||
@@ -2365,10 +2422,9 @@ class Decoder(srd.Decoder):
|
|||||||
packet.append(sw2)
|
packet.append(sw2)
|
||||||
got_sw = True
|
got_sw = True
|
||||||
break
|
break
|
||||||
self._note_discard(b,
|
self._note_pre_sw(b,
|
||||||
'non-SW byte in P3=0 '
|
'non-SW byte in P3=0 '
|
||||||
'response',
|
'response')
|
||||||
idle=(b in (0x00, 0xFF)))
|
|
||||||
else:
|
else:
|
||||||
# Case 3/4: P3 command-data bytes, then
|
# Case 3/4: P3 command-data bytes, then
|
||||||
# procedure byte.
|
# procedure byte.
|
||||||
@@ -2388,8 +2444,7 @@ class Decoder(srd.Decoder):
|
|||||||
b = self.read_byte()
|
b = self.read_byte()
|
||||||
if b is None:
|
if b is None:
|
||||||
return True
|
return True
|
||||||
if ((b & 0xF0) == 0x60
|
if plausible_sw(b):
|
||||||
or (b & 0xF0) == 0x90):
|
|
||||||
sw2 = self.read_byte()
|
sw2 = self.read_byte()
|
||||||
if sw2 is None:
|
if sw2 is None:
|
||||||
return True
|
return True
|
||||||
@@ -2397,10 +2452,9 @@ class Decoder(srd.Decoder):
|
|||||||
packet.append(sw2)
|
packet.append(sw2)
|
||||||
got_sw = True
|
got_sw = True
|
||||||
break
|
break
|
||||||
self._note_discard(b,
|
self._note_pre_sw(b,
|
||||||
'non-SW byte in case-4 '
|
'non-SW byte in case-4 '
|
||||||
'response',
|
'response')
|
||||||
idle=(b in (0x00, 0xFF)))
|
|
||||||
break
|
break
|
||||||
elif (pb == (bIns ^ 0x01)
|
elif (pb == (bIns ^ 0x01)
|
||||||
or pb == ((bIns ^ 0xFF) ^ 0x01)):
|
or pb == ((bIns ^ 0xFF) ^ 0x01)):
|
||||||
@@ -2430,8 +2484,7 @@ class Decoder(srd.Decoder):
|
|||||||
return True
|
return True
|
||||||
packet.append(b)
|
packet.append(b)
|
||||||
continue
|
continue
|
||||||
elif ((pb & 0xF0) == 0x60
|
elif plausible_sw(pb):
|
||||||
or (pb & 0xF0) == 0x90):
|
|
||||||
# SW1: case 3, no response data.
|
# SW1: case 3, no response data.
|
||||||
sw2 = self.read_byte()
|
sw2 = self.read_byte()
|
||||||
if sw2 is None:
|
if sw2 is None:
|
||||||
@@ -2445,8 +2498,7 @@ class Decoder(srd.Decoder):
|
|||||||
# skip non-SW bytes (e.g. 0xFF
|
# skip non-SW bytes (e.g. 0xFF
|
||||||
# turnaround artifacts).
|
# turnaround artifacts).
|
||||||
for _ in range(MAX_TPDU_LEN):
|
for _ in range(MAX_TPDU_LEN):
|
||||||
if ((pb & 0xF0) == 0x60
|
if plausible_sw(pb):
|
||||||
or (pb & 0xF0) == 0x90):
|
|
||||||
sw2 = self.read_byte()
|
sw2 = self.read_byte()
|
||||||
if sw2 is None:
|
if sw2 is None:
|
||||||
return True
|
return True
|
||||||
@@ -2454,10 +2506,9 @@ class Decoder(srd.Decoder):
|
|||||||
packet.append(sw2)
|
packet.append(sw2)
|
||||||
got_sw = True
|
got_sw = True
|
||||||
break
|
break
|
||||||
self._note_discard(pb,
|
self._note_pre_sw(pb,
|
||||||
'non-SW byte during '
|
'non-SW byte during '
|
||||||
'unexpected-byte scan',
|
'unexpected-byte scan')
|
||||||
idle=(pb in (0x00, 0xFF)))
|
|
||||||
pb = self.read_byte()
|
pb = self.read_byte()
|
||||||
if pb is None:
|
if pb is None:
|
||||||
return True
|
return True
|
||||||
@@ -2503,7 +2554,7 @@ class Decoder(srd.Decoder):
|
|||||||
if b is None:
|
if b is None:
|
||||||
break
|
break
|
||||||
packet.append(b)
|
packet.append(b)
|
||||||
elif ((pb & 0xF0) == 0x60 or (pb & 0xF0) == 0x90):
|
elif plausible_sw(pb):
|
||||||
# SW1: append SW2; the exchange is complete.
|
# SW1: append SW2; the exchange is complete.
|
||||||
sw2 = self.read_byte()
|
sw2 = self.read_byte()
|
||||||
if sw2 is None:
|
if sw2 is None:
|
||||||
|
|||||||
+17
-11
@@ -1,7 +1,7 @@
|
|||||||
=== Baseline captured 2026-09-17T21:58:59+03:00 ===
|
=== Baseline captured 2026-09-19T00:47:38+03:00 ===
|
||||||
|
|
||||||
=== test_8 ===
|
=== test_8 ===
|
||||||
('=== iso7816 decoder v1.8.0 ===',)
|
('=== iso7816 decoder v1.9.1 ===',)
|
||||||
('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.8.0 ===',)
|
('=== iso7816 decoder v1.9.1 ===',)
|
||||||
('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.8.0 ===',)
|
('=== iso7816 decoder v1.9.1 ===',)
|
||||||
('etu recovered (samples):', 53)
|
('etu recovered (samples):', 53)
|
||||||
(11673977, 'CHKSUM ERROR: ', 0, 'bits: ', [0, 0, 0, 1, 0, 0, 1, 1, 1, 1])
|
(11673977, '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]
|
||||||
@@ -118,7 +118,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.8.0 ===',)
|
('=== iso7816 decoder v1.9.1 ===',)
|
||||||
('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',)
|
||||||
@@ -145,7 +145,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.8.0 ===',)
|
('=== iso7816 decoder v1.9.1 ===',)
|
||||||
('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',)
|
||||||
@@ -186,7 +186,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.8.0 ===',)
|
('=== iso7816 decoder v1.9.1 ===',)
|
||||||
('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',)
|
||||||
@@ -213,7 +213,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.8.0 ===',)
|
('=== iso7816 decoder v1.9.1 ===',)
|
||||||
('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',)
|
||||||
@@ -240,7 +240,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.8.0 ===',)
|
('=== iso7816 decoder v1.9.1 ===',)
|
||||||
('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',)
|
||||||
@@ -267,7 +267,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.8.0 ===',)
|
('=== iso7816 decoder v1.9.1 ===',)
|
||||||
('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',)
|
||||||
@@ -368,6 +368,7 @@ test_short_pulse_start_edge_preserved (__main__.TestDeglitchEdges.test_short_pul
|
|||||||
test_is_desynced_all_zero_is_not_desync (__main__.TestDesyncHelpers.test_is_desynced_all_zero_is_not_desync) ... ok
|
test_is_desynced_all_zero_is_not_desync (__main__.TestDesyncHelpers.test_is_desynced_all_zero_is_not_desync) ... ok
|
||||||
test_is_desynced_bad_sw (__main__.TestDesyncHelpers.test_is_desynced_bad_sw) ... ok
|
test_is_desynced_bad_sw (__main__.TestDesyncHelpers.test_is_desynced_bad_sw) ... ok
|
||||||
test_is_desynced_clean_packets (__main__.TestDesyncHelpers.test_is_desynced_clean_packets) ... ok
|
test_is_desynced_clean_packets (__main__.TestDesyncHelpers.test_is_desynced_clean_packets) ... ok
|
||||||
|
test_is_desynced_null_sw1_is_invalid (__main__.TestDesyncHelpers.test_is_desynced_null_sw1_is_invalid) ... ok
|
||||||
test_is_desynced_oversized (__main__.TestDesyncHelpers.test_is_desynced_oversized) ... ok
|
test_is_desynced_oversized (__main__.TestDesyncHelpers.test_is_desynced_oversized) ... ok
|
||||||
test_is_desynced_short_fragment (__main__.TestDesyncHelpers.test_is_desynced_short_fragment) ... ok
|
test_is_desynced_short_fragment (__main__.TestDesyncHelpers.test_is_desynced_short_fragment) ... ok
|
||||||
test_max_tpdu_len (__main__.TestDesyncHelpers.test_max_tpdu_len) ... ok
|
test_max_tpdu_len (__main__.TestDesyncHelpers.test_max_tpdu_len) ... ok
|
||||||
@@ -392,6 +393,11 @@ test_build_packet_payload_passthrough (__main__.TestGsmtapFlags.test_build_packe
|
|||||||
test_build_packet_plain_res_zero (__main__.TestGsmtapFlags.test_build_packet_plain_res_zero) ... ok
|
test_build_packet_plain_res_zero (__main__.TestGsmtapFlags.test_build_packet_plain_res_zero) ... ok
|
||||||
test_flag_constant_value (__main__.TestGsmtapFlags.test_flag_constant_value) ... ok
|
test_flag_constant_value (__main__.TestGsmtapFlags.test_flag_constant_value) ... ok
|
||||||
test_pps_subtypes_are_standard (__main__.TestGsmtapFlags.test_pps_subtypes_are_standard) ... ok
|
test_pps_subtypes_are_standard (__main__.TestGsmtapFlags.test_pps_subtypes_are_standard) ... ok
|
||||||
|
test_base_payload_without_rate (__main__.TestLineEventPayload.test_base_payload_without_rate) ... ok
|
||||||
|
test_flag_constant (__main__.TestLineEventPayload.test_flag_constant) ... ok
|
||||||
|
test_legacy_prefix_preserved (__main__.TestLineEventPayload.test_legacy_prefix_preserved) ... ok
|
||||||
|
test_payload_with_clk_rate (__main__.TestLineEventPayload.test_payload_with_clk_rate) ... ok
|
||||||
|
test_rate_absent_when_unknown (__main__.TestLineEventPayload.test_rate_absent_when_unknown) ... ok
|
||||||
test_accepts_full_hold_fall (__main__.TestNextStartFall.test_accepts_full_hold_fall) ... ok
|
test_accepts_full_hold_fall (__main__.TestNextStartFall.test_accepts_full_hold_fall) ... ok
|
||||||
test_finds_fall_after_threshold (__main__.TestNextStartFall.test_finds_fall_after_threshold) ... ok
|
test_finds_fall_after_threshold (__main__.TestNextStartFall.test_finds_fall_after_threshold) ... ok
|
||||||
test_finds_fall_exactly_at_threshold (__main__.TestNextStartFall.test_finds_fall_exactly_at_threshold)
|
test_finds_fall_exactly_at_threshold (__main__.TestNextStartFall.test_finds_fall_exactly_at_threshold)
|
||||||
@@ -419,6 +425,6 @@ test_valid_cla_interindustry_classes (__main__.TestValidateT0Apdu.test_valid_cla
|
|||||||
test_valid_direct_sw_with_p3_nonzero (__main__.TestValidateT0Apdu.test_valid_direct_sw_with_p3_nonzero) ... ok
|
test_valid_direct_sw_with_p3_nonzero (__main__.TestValidateT0Apdu.test_valid_direct_sw_with_p3_nonzero) ... ok
|
||||||
|
|
||||||
----------------------------------------------------------------------
|
----------------------------------------------------------------------
|
||||||
Ran 68 tests in 0.027s
|
Ran 74 tests in 0.027s
|
||||||
|
|
||||||
OK
|
OK
|
||||||
+51
-4
@@ -79,23 +79,36 @@ class TestDesyncHelpers(unittest.TestCase):
|
|||||||
cls.gs, cls.pd = _load_iso7816()
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
def test_plausible_sw_accepts_valid_classes(self):
|
def test_plausible_sw_accepts_valid_classes(self):
|
||||||
for sw1 in list(range(0x60, 0x70)) + list(range(0x90, 0xA0)):
|
# ISO 7816-3 §10.3.3 Table 11: SW1 is '6X' or '9X', except '60'
|
||||||
|
# (the NULL procedure byte).
|
||||||
|
for sw1 in list(range(0x61, 0x70)) + list(range(0x90, 0xA0)):
|
||||||
self.assertTrue(self.pd.plausible_sw(sw1), hex(sw1))
|
self.assertTrue(self.pd.plausible_sw(sw1), hex(sw1))
|
||||||
|
|
||||||
def test_plausible_sw_rejects_garbage(self):
|
def test_plausible_sw_rejects_garbage(self):
|
||||||
for sw1 in (0x00, 0x01, 0x3B, 0x3F, 0xA0, 0xFF, 0x88, 0x5F):
|
for sw1 in (0x00, 0x01, 0x3B, 0x3F, 0x5F, 0x60, 0x88, 0xA0, 0xFF):
|
||||||
self.assertFalse(self.pd.plausible_sw(sw1), hex(sw1))
|
self.assertFalse(self.pd.plausible_sw(sw1), hex(sw1))
|
||||||
|
|
||||||
def test_max_tpdu_len(self):
|
def test_max_tpdu_len(self):
|
||||||
self.assertEqual(self.pd.MAX_TPDU_LEN, 271)
|
self.assertEqual(self.pd.MAX_TPDU_LEN, 271)
|
||||||
|
|
||||||
def test_is_desynced_clean_packets(self):
|
def test_is_desynced_clean_packets(self):
|
||||||
# 7-byte: header(5) + proc(1) + SW2(1) ending in a 9x SW1
|
# header(5) + SW1 SW2 (P3=0 direct status word)
|
||||||
self.assertFalse(self.pd.is_desynced(b'\x00\xa4\x00\x04\x02\x60\x90'))
|
self.assertFalse(self.pd.is_desynced(b'\x00\xa4\x00\x04\x00\x90\x00'))
|
||||||
# ACK-style GET RESPONSE (header + proc + payload + SW 9000)
|
# ACK-style GET RESPONSE (header + proc + payload + SW 9000)
|
||||||
self.assertFalse(self.pd.is_desynced(
|
self.assertFalse(self.pd.is_desynced(
|
||||||
b'\x00\xc0\x00\x00\x25\xc0\x62\x23\x90\x00'))
|
b'\x00\xc0\x00\x00\x25\xc0\x62\x23\x90\x00'))
|
||||||
|
|
||||||
|
def test_is_desynced_null_sw1_is_invalid(self):
|
||||||
|
# Regression (v1.9.1): the T=0 NULL byte 0x60 is not SW1. The
|
||||||
|
# wire "NULL 0x60 + real SW1 0x61" was mis-framed as the status
|
||||||
|
# word "60 61" (real SW1 eaten as SW2). It must be flagged, not
|
||||||
|
# accepted (trace: SELECT MF -> NULL wait -> 61 29).
|
||||||
|
pkt = b'\x00\xa4\x00\x04\x02\x3f\x00\x60\x61'
|
||||||
|
valid, reason = self.pd.validate_t0_apdu(pkt)
|
||||||
|
self.assertFalse(valid, 'NULL must not pass as SW1')
|
||||||
|
self.assertIn('invalid SW1', reason)
|
||||||
|
self.assertTrue(self.pd.is_desynced(pkt))
|
||||||
|
|
||||||
def test_is_desynced_short_fragment(self):
|
def test_is_desynced_short_fragment(self):
|
||||||
# 5-byte header-only (INVALID-procedure residue) -- no SW read
|
# 5-byte header-only (INVALID-procedure residue) -- no SW read
|
||||||
self.assertTrue(self.pd.is_desynced(b'\xa4\x3f\x00\x61\x25'))
|
self.assertTrue(self.pd.is_desynced(b'\xa4\x3f\x00\x61\x25'))
|
||||||
@@ -568,5 +581,39 @@ class TestEndOfStream(unittest.TestCase):
|
|||||||
self._decode_raising(SystemError).decode()
|
self._decode_raising(SystemError).decode()
|
||||||
|
|
||||||
|
|
||||||
|
class TestLineEventPayload(unittest.TestCase):
|
||||||
|
"""RST/VCC line-event payloads (GSMTAP sub_type 0x10/0x11), including the
|
||||||
|
CLK-frequency extension (v1.9.0)."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
cls.gs, cls.pd = _load_iso7816()
|
||||||
|
|
||||||
|
def test_base_payload_without_rate(self):
|
||||||
|
self.assertEqual(self.pd.line_event_payload(0, 1), b'\x00\x01\x00')
|
||||||
|
self.assertEqual(self.pd.line_event_payload(1, 0, None), b'\x01\x00\x00')
|
||||||
|
|
||||||
|
def test_payload_with_clk_rate(self):
|
||||||
|
p = self.pd.line_event_payload(0, 1, 4800000)
|
||||||
|
self.assertEqual(len(p), 7)
|
||||||
|
self.assertEqual(p[0], 0) # deasserted
|
||||||
|
self.assertEqual(p[1], 1) # resulting level: high
|
||||||
|
self.assertEqual(p[2], self.pd.LINE_EVENT_FLAG_CLK_HZ)
|
||||||
|
self.assertEqual(int.from_bytes(p[3:7], 'big'), 4800000)
|
||||||
|
|
||||||
|
def test_rate_absent_when_unknown(self):
|
||||||
|
self.assertEqual(len(self.pd.line_event_payload(1, 0, None)), 3)
|
||||||
|
self.assertEqual(len(self.pd.line_event_payload(1, 0, 0)), 3)
|
||||||
|
|
||||||
|
def test_legacy_prefix_preserved(self):
|
||||||
|
# Consumers that read only bytes 0-1 (pysniff decode_line_event)
|
||||||
|
# must see the same direction/level as before.
|
||||||
|
p = self.pd.line_event_payload(1, 0, 3579000)
|
||||||
|
self.assertEqual(p[:2], b'\x01\x00')
|
||||||
|
|
||||||
|
def test_flag_constant(self):
|
||||||
|
self.assertEqual(self.pd.LINE_EVENT_FLAG_CLK_HZ, 0x01)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
+3
-1
@@ -126,7 +126,9 @@ def embedded_exchanges(buf):
|
|||||||
|
|
||||||
|
|
||||||
def _plausible_sw(sw1):
|
def _plausible_sw(sw1):
|
||||||
return (0x60 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
# Mirror of plausible_sw in pd.py: SW1 is '6X'/'9X' except '60'
|
||||||
|
# (the T=0 NULL procedure byte; ISO 7816-4 makes it an invalid SW1).
|
||||||
|
return (0x61 <= sw1 <= 0x6F) or (0x90 <= sw1 <= 0x9F)
|
||||||
|
|
||||||
|
|
||||||
def _plausible_ins(ins):
|
def _plausible_ins(ins):
|
||||||
|
|||||||
Reference in new issue
Block a user