nfcscript 0.0.12__tar.gz → 0.0.14__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. {nfcscript-0.0.12 → nfcscript-0.0.14}/PKG-INFO +3 -2
  2. {nfcscript-0.0.12 → nfcscript-0.0.14}/README.md +2 -1
  3. {nfcscript-0.0.12 → nfcscript-0.0.14}/pyproject.toml +2 -2
  4. nfcscript-0.0.14/src/nfc/hex_util.py +189 -0
  5. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfc/reader.py +16 -15
  6. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfcscript.egg-info/PKG-INFO +3 -2
  7. nfcscript-0.0.14/tests/test_hex_util.py +245 -0
  8. nfcscript-0.0.12/src/nfc/hex_util.py +0 -131
  9. nfcscript-0.0.12/tests/test_hex_util.py +0 -168
  10. {nfcscript-0.0.12 → nfcscript-0.0.14}/setup.cfg +0 -0
  11. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfc/__init__.py +0 -0
  12. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfc/assertions.py +0 -0
  13. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfc/bits.py +0 -0
  14. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfc/checksum.py +0 -0
  15. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfc/cli.py +0 -0
  16. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfc/delay.py +0 -0
  17. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfc/nfc_cli.py +0 -0
  18. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfc/trace.py +0 -0
  19. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfcscript.egg-info/SOURCES.txt +0 -0
  20. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfcscript.egg-info/dependency_links.txt +0 -0
  21. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfcscript.egg-info/entry_points.txt +0 -0
  22. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfcscript.egg-info/requires.txt +0 -0
  23. {nfcscript-0.0.12 → nfcscript-0.0.14}/src/nfcscript.egg-info/top_level.txt +0 -0
  24. {nfcscript-0.0.12 → nfcscript-0.0.14}/tests/test_assertions.py +0 -0
  25. {nfcscript-0.0.12 → nfcscript-0.0.14}/tests/test_bits.py +0 -0
  26. {nfcscript-0.0.12 → nfcscript-0.0.14}/tests/test_checksum.py +0 -0
  27. {nfcscript-0.0.12 → nfcscript-0.0.14}/tests/test_cli.py +0 -0
  28. {nfcscript-0.0.12 → nfcscript-0.0.14}/tests/test_cli_collect.py +0 -0
  29. {nfcscript-0.0.12 → nfcscript-0.0.14}/tests/test_delay.py +0 -0
  30. {nfcscript-0.0.12 → nfcscript-0.0.14}/tests/test_reader.py +0 -0
  31. {nfcscript-0.0.12 → nfcscript-0.0.14}/tests/test_trace.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nfcscript
3
- Version: 0.0.12
3
+ Version: 0.0.14
4
4
  Summary: A plugin-based core engine for DSL-style NFC testing automation.
5
5
  Author: crthu
6
6
  Requires-Python: >=3.14
@@ -211,7 +211,8 @@ with session() as reader:
211
211
 
212
212
  | 函数 | 说明 |
213
213
  |------|------|
214
- | `PARSE_HEX(text)` | 解析 hex 字符串 |
214
+ | `PARSE_HEX(text)` | 解析纯 hex,返回 `list[int]` |
215
+ | `PARSE_HEX_BITS(text, last_bits=None)` | 解析 hex + Verilog 位标记,返回 `tuple[list[int], int]` |
215
216
  | `FORMAT_HEX(data, last_bits)` | 格式化为可视化 hex |
216
217
 
217
218
  ### checksum.py
@@ -201,7 +201,8 @@ with session() as reader:
201
201
 
202
202
  | 函数 | 说明 |
203
203
  |------|------|
204
- | `PARSE_HEX(text)` | 解析 hex 字符串 |
204
+ | `PARSE_HEX(text)` | 解析纯 hex,返回 `list[int]` |
205
+ | `PARSE_HEX_BITS(text, last_bits=None)` | 解析 hex + Verilog 位标记,返回 `tuple[list[int], int]` |
205
206
  | `FORMAT_HEX(data, last_bits)` | 格式化为可视化 hex |
206
207
 
207
208
  ### checksum.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nfcscript"
3
- version = "0.0.12"
3
+ version = "0.0.14"
4
4
  description = "A plugin-based core engine for DSL-style NFC testing automation."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.14"
@@ -34,7 +34,7 @@ dev = [
34
34
  ]
35
35
 
36
36
  [tool.bumpversion]
37
- current_version = "0.0.12"
37
+ current_version = "0.0.14"
38
38
  commit = true
39
39
  tag = true
40
40
  allow_dirty = true
@@ -0,0 +1,189 @@
1
+ import re
2
+
3
+ # 位标记正则:匹配 N'hXX 格式 (如 7'h26, 4'hC)
4
+ BIT_PATTERN = re.compile(r"(\d+)\s*\'h\s*([0-9a-fA-F]+)")
5
+
6
+
7
+ def PARSE_HEX(text: str) -> list[int]:
8
+ """
9
+ 解析纯 hex 字符串为 list[int]。
10
+
11
+ Args:
12
+ text: 十六进制文本,支持空格分隔 (如 "AA BB CC" 或 "AABBCC")。
13
+
14
+ Returns:
15
+ list[int]: 解析后的字节列表。
16
+
17
+ Raises:
18
+ ValueError: 不合法的十六进制数据时抛出。
19
+
20
+ Examples:
21
+ >>> PARSE_HEX("AA BB CC")
22
+ [0xAA, 0xBB, 0xCC]
23
+ >>> PARSE_HEX("0011")
24
+ [0x00, 0x11]
25
+ """
26
+ # 去空格得到纯 hex
27
+ clean_hex = text.replace(" ", "").upper()
28
+
29
+ # 简单的合法性检查
30
+ if clean_hex:
31
+ try:
32
+ return list(bytes.fromhex(clean_hex))
33
+ except ValueError:
34
+ raise ValueError(f"不合法的十六进制数据: {clean_hex}")
35
+ return []
36
+
37
+
38
+ def PARSE_HEX_BITS(text: str, last_bits: int | None = None) -> tuple[list[int], int]:
39
+ """
40
+ 解析 hex 字符串为 tuple[list[int], int],支持 Verilog 位标记。
41
+
42
+ 支持格式:
43
+ - 纯 hex: "AA BB CC" → ([0xAA, 0xBB, 0xCC], 0)
44
+ - Verilog 标记: "AA 4'hBB" → ([0xAA, 0x0B], 4)
45
+ - 混合: "AA BB 7'h03" → ([0xAA, 0xBB, 0x03], 7)
46
+ - 手动指定: "AA BB", last_bits=4 → ([0xAA, 0xBB], 4)
47
+
48
+ Args:
49
+ text: 十六进制文本,支持空格分隔和 N'hXX 位标记。
50
+ last_bits: 可选的最后字节有效位数,与 Verilog 标记冲突时报错。
51
+
52
+ Returns:
53
+ tuple[list[int], int]: (字节列表, 最后字节有效位数)。
54
+
55
+ Raises:
56
+ ValueError: 不合法的十六进制数据、位标记冲突或多标记时报错。
57
+
58
+ Examples:
59
+ >>> PARSE_HEX_BITS("AA BB CC")
60
+ ([0xAA, 0xBB, 0xCC], 0)
61
+ >>> PARSE_HEX_BITS("AA 4'hBB")
62
+ ([0xAA, 0x0B], 4)
63
+ >>> PARSE_HEX_BITS("0011", last_bits=4)
64
+ ([0x00, 0x01], 4)
65
+ >>> PARSE_HEX_BITS("AA 4'hBB", last_bits=4)
66
+ ValueError: 冲突
67
+ """
68
+ # 冲突检测:只允许一个 Verilog 标记
69
+ matches = list(BIT_PATTERN.finditer(text))
70
+ if len(matches) > 1:
71
+ raise ValueError("只允许一个 Verilog 位标记")
72
+ if matches and last_bits is not None:
73
+ raise ValueError("last_bits 参数与 Verilog 位标记冲突")
74
+
75
+ bits = last_bits if last_bits is not None else 0
76
+
77
+ # 处理 Verilog 位标记
78
+ if matches:
79
+ m = matches[0]
80
+ n_bits = int(m.group(1))
81
+ bits = n_bits
82
+ # 按 bits 截断: 4'hCA → 0x0A
83
+ val = int(m.group(2), 16) & ((1 << n_bits) - 1)
84
+ hex_val = format(val, 'X')
85
+ if len(hex_val) % 2 != 0:
86
+ hex_val = '0' + hex_val
87
+ text = text[:m.start()] + hex_val + text[m.end():]
88
+
89
+ # 去空格得到纯 hex
90
+ clean_hex = text.replace(" ", "").upper()
91
+
92
+ # 解析 hex 数据
93
+ if clean_hex:
94
+ try:
95
+ data = list(bytes.fromhex(clean_hex))
96
+ except ValueError:
97
+ raise ValueError(f"不合法的十六进制数据: {clean_hex}")
98
+ else:
99
+ data = []
100
+
101
+ # last_bits 截断最后一个字节
102
+ if bits and data:
103
+ data[-1] = data[-1] & ((1 << bits) - 1)
104
+
105
+ return data, bits
106
+
107
+
108
+ def FORMAT_HEX(data: int | bytes | list[int] | str, last_bits: int = 0) -> str:
109
+ """
110
+ 格式化数据为带位标记的可视化格式。
111
+
112
+ 支持输入类型: int, bytes, list[int], str (纯 hex)。
113
+
114
+ Args:
115
+ data: 待格式化的数据。
116
+ last_bits: 最后一个字节的有效位数 (1-7),0 表示整字节。
117
+
118
+ Returns:
119
+ str: 格式化后的字符串。
120
+
121
+ Raises:
122
+ TypeError: 不支持的输入类型时抛出。
123
+
124
+ Examples:
125
+ >>> FORMAT_HEX(0xAABB03, 7)
126
+ "AA BB 7'h03"
127
+ >>> FORMAT_HEX([0xAA, 0xBB, 0x03])
128
+ 'AA BB 03'
129
+ >>> FORMAT_HEX("AABB", 4)
130
+ "AA 4'hBB"
131
+ """
132
+ # 1. 统一转换为纯十六进制字符串
133
+ if isinstance(data, int):
134
+ hex_str = data.to_bytes(max(1, (data.bit_length() + 7) // 8)).hex().upper()
135
+ elif isinstance(data, bytes):
136
+ hex_str = data.hex().upper()
137
+ elif isinstance(data, list):
138
+ hex_str = bytes(data).hex().upper()
139
+ elif isinstance(data, str):
140
+ hex_str = data.replace(" ", "").upper()
141
+ else:
142
+ raise TypeError(f"不支持的输入类型: {type(data)}")
143
+
144
+ if not hex_str:
145
+ return ""
146
+
147
+ # 2. 如果是整字节 (无 last_bits)
148
+ if last_bits == 0:
149
+ return ' '.join(hex_str[i:i+2] for i in range(0, len(hex_str), 2))
150
+
151
+ # 3. 有 last_bits,处理最后一个字节
152
+ if len(hex_str) <= 2:
153
+ return f"{last_bits}'h{hex_str}"
154
+
155
+ prefix = hex_str[:-2]
156
+ last_byte = hex_str[-2:]
157
+ formatted_prefix = ' '.join(prefix[i:i+2] for i in range(0, len(prefix), 2))
158
+ return f"{formatted_prefix} {last_bits}'h{last_byte}"
159
+
160
+
161
+ if __name__ == "__main__":
162
+ # --- 测试例程 ---
163
+ print("=== Hex 工具测试 ===")
164
+
165
+ # 测试用例: (data, last_bits, 类型名)
166
+ test_cases = [
167
+ ("AABB03", 7, "str"),
168
+ (0xAABB03, 7, "int"),
169
+ (b'\xAA\xBB\x03', 7, "bytes"),
170
+ ([0xAA, 0xBB, 0x03], 7, "list"),
171
+ ("FF", 4, "str"),
172
+ (0xFF, 4, "int"),
173
+ ("AABBCC", 0, "str"),
174
+ (0xAABBCC, 0, "int"),
175
+ (b'\xAA\xBB\xCC', 0, "bytes"),
176
+ ]
177
+
178
+ for data, b, typename in test_cases:
179
+ formatted = FORMAT_HEX(data, b)
180
+ print(f"输入: ({data}, {b}, {typename}) -> 格式化: {formatted}")
181
+
182
+ # 反向解析验证
183
+ if b:
184
+ parsed, bits = PARSE_HEX_BITS(formatted)
185
+ print(f"解析: {formatted} -> Hex: {parsed}, Bits: {bits}")
186
+ else:
187
+ parsed = PARSE_HEX(formatted)
188
+ print(f"解析: {formatted} -> Hex: {parsed}")
189
+ print("-" * 20)
@@ -103,7 +103,7 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
103
103
  # 手动底层寻卡流程
104
104
  # 1. REQA
105
105
  res_reqa = reqa(cmd=reqa_cmd)
106
- if not res_reqa:
106
+ if not res_reqa.data:
107
107
  return None
108
108
  atq = res_reqa.data
109
109
 
@@ -111,7 +111,7 @@ def active(low_layer: bool = False, ignore_error: bool = False, reqa_cmd: int =
111
111
  full_uid = []
112
112
  for cl in [1, 2, 3]:
113
113
  res = anticoll(cl_level=cl, nvb=0x20)
114
- if not res or not res.data:
114
+ if not res.data:
115
115
  return None
116
116
 
117
117
  data = res.data
@@ -171,7 +171,7 @@ def transceive(data: list[int], tx_crc: bool = True, rx_crc: bool = True) -> lis
171
171
  return res.data if res and res.data else []
172
172
 
173
173
 
174
- def transceive_bits(data: list[int], last_tx_bits: int = 0, tx_crc: bool = True, rx_crc: bool = True) -> TransceiveBits | None:
174
+ def transceive_bits(data: list[int], last_tx_bits: int = 0, tx_crc: bool = True, rx_crc: bool = True) -> TransceiveBits:
175
175
  """
176
176
  使用底层帧交互方式发送数据,并支持对最后一个字节进行位控制。
177
177
 
@@ -182,14 +182,15 @@ def transceive_bits(data: list[int], last_tx_bits: int = 0, tx_crc: bool = True,
182
182
  rx_crc: 接收时是否自动校验 CRC。False 则原样接收不校验。
183
183
 
184
184
  Returns:
185
- TransceiveBits | None: 包含 data 和 bits 属性,失败返回 None。
185
+ TransceiveBits: 包含 data 和 bits 属性,失败返回空的 TransceiveBits(data=[], bits=0)。
186
186
  """
187
187
  _state.ensure_connected()
188
188
  reader = _state.reader
189
- return reader.transceive(data, last_tx_bits=last_tx_bits, tx_crc=tx_crc, rx_crc=rx_crc)
189
+ res = reader.transceive(data, last_tx_bits=last_tx_bits, tx_crc=tx_crc, rx_crc=rx_crc)
190
+ return res if res else TransceiveBits(data=[], bits=0)
190
191
 
191
192
 
192
- def reqa(cmd: int = 0x26) -> TransceiveBits | None:
193
+ def reqa(cmd: int = 0x26) -> TransceiveBits:
193
194
  """
194
195
  ISO14443-A REQA (7 bits)。
195
196
 
@@ -197,27 +198,27 @@ def reqa(cmd: int = 0x26) -> TransceiveBits | None:
197
198
  cmd: REQA 命令字节,默认 0x26。
198
199
 
199
200
  Returns:
200
- TransceiveBits | None: 包含 data 和 bits 属性,失败返回 None。
201
+ TransceiveBits: 包含 data 和 bits 属性,失败返回空的 TransceiveBits(data=[], bits=0)。
201
202
  """
202
203
  return transceive_bits([cmd], last_tx_bits=7, tx_crc=False, rx_crc=False)
203
204
 
204
205
 
205
- def wupa() -> TransceiveBits | None:
206
+ def wupa() -> TransceiveBits:
206
207
  """
207
208
  ISO14443-A WUPA (7 bits)。
208
209
 
209
210
  Returns:
210
- TransceiveBits | None: 包含 data 和 bits 属性,失败返回 None。
211
+ TransceiveBits: 包含 data 和 bits 属性,失败返回空的 TransceiveBits(data=[], bits=0)。
211
212
  """
212
213
  return transceive_bits([0x52], last_tx_bits=7, tx_crc=False, rx_crc=False)
213
214
 
214
215
 
215
- def halt() -> list[int] | None:
216
+ def halt() -> list[int]:
216
217
  """
217
218
  ISO14443-A HALT (Standard Frame)。
218
219
 
219
220
  Returns:
220
- list[int] | None: 返回响应数据,失败返回 None。
221
+ list[int]: 返回响应数据。
221
222
  """
222
223
  return transceive([0x50, 0x00], tx_crc=True, rx_crc=True)
223
224
 
@@ -241,7 +242,7 @@ def _get_sel_cmd(cl_level: int) -> int:
241
242
  return sel_map[cl_level]
242
243
 
243
244
 
244
- def anticoll(cl_level: int, nvb: int = 0x20, uid_prefix: list[int] = []) -> TransceiveBits | None:
245
+ def anticoll(cl_level: int, nvb: int = 0x20, uid_prefix: list[int] = []) -> TransceiveBits:
245
246
  """
246
247
  ISO14443-A ANTICOLL (Anti-collision)。
247
248
 
@@ -251,13 +252,13 @@ def anticoll(cl_level: int, nvb: int = 0x20, uid_prefix: list[int] = []) -> Tran
251
252
  uid_prefix: 已知的部分 UID (0-4 字节)。
252
253
 
253
254
  Returns:
254
- TransceiveBits | None: 包含 data 和 bits 属性,失败返回 None。
255
+ TransceiveBits: 包含 data 和 bits 属性,失败返回空的 TransceiveBits(data=[], bits=0)。
255
256
  """
256
257
  cmd = [_get_sel_cmd(cl_level), nvb] + uid_prefix
257
258
  return transceive_bits(cmd, last_tx_bits=0, tx_crc=False, rx_crc=False)
258
259
 
259
260
 
260
- def select(cl_level: int, uid: list[int]) -> list[int] | None:
261
+ def select(cl_level: int, uid: list[int]) -> list[int]:
261
262
  """
262
263
  ISO14443-A SELECT (Standard Frame)。
263
264
 
@@ -266,7 +267,7 @@ def select(cl_level: int, uid: list[int]) -> list[int] | None:
266
267
  uid: 完整 5 字节 UID (包含 BCC)。
267
268
 
268
269
  Returns:
269
- list[int] | None: 返回响应数据,失败返回 None。
270
+ list[int]: 返回响应数据。
270
271
  """
271
272
  cmd = [_get_sel_cmd(cl_level), 0x70] + uid
272
273
  return transceive(cmd, tx_crc=True, rx_crc=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nfcscript
3
- Version: 0.0.12
3
+ Version: 0.0.14
4
4
  Summary: A plugin-based core engine for DSL-style NFC testing automation.
5
5
  Author: crthu
6
6
  Requires-Python: >=3.14
@@ -211,7 +211,8 @@ with session() as reader:
211
211
 
212
212
  | 函数 | 说明 |
213
213
  |------|------|
214
- | `PARSE_HEX(text)` | 解析 hex 字符串 |
214
+ | `PARSE_HEX(text)` | 解析纯 hex,返回 `list[int]` |
215
+ | `PARSE_HEX_BITS(text, last_bits=None)` | 解析 hex + Verilog 位标记,返回 `tuple[list[int], int]` |
215
216
  | `FORMAT_HEX(data, last_bits)` | 格式化为可视化 hex |
216
217
 
217
218
  ### checksum.py
@@ -0,0 +1,245 @@
1
+ """PARSE_HEX / PARSE_HEX_BITS / FORMAT_HEX 测试"""
2
+ import pytest
3
+ from nfc.hex_util import PARSE_HEX, PARSE_HEX_BITS, FORMAT_HEX
4
+
5
+
6
+ class TestPARSE_HEX:
7
+ """PARSE_HEX: 纯 hex 解析,返回 list[int]"""
8
+
9
+ def test_simple_hex(self):
10
+ assert PARSE_HEX("AA BB CC") == [0xAA, 0xBB, 0xCC]
11
+
12
+ def test_no_space(self):
13
+ assert PARSE_HEX("AABBCC") == [0xAA, 0xBB, 0xCC]
14
+
15
+ def test_empty_string(self):
16
+ assert PARSE_HEX("") == []
17
+
18
+ def test_whitespace_only(self):
19
+ assert PARSE_HEX(" ") == []
20
+
21
+ def test_single_byte(self):
22
+ assert PARSE_HEX("FF") == [0xFF]
23
+
24
+ def test_lowercase(self):
25
+ assert PARSE_HEX("ff aa") == [0xFF, 0xAA]
26
+
27
+ def test_invalid_hex_raises(self):
28
+ with pytest.raises(ValueError, match="不合法的十六进制数据"):
29
+ PARSE_HEX("ZZZZ")
30
+
31
+ def test_odd_hex_digits_raises(self):
32
+ with pytest.raises(ValueError):
33
+ PARSE_HEX("ABC")
34
+
35
+
36
+ class TestPARSE_HEX_BITS:
37
+ """PARSE_HEX_BITS: 返回 tuple[list[int], int],支持位截断"""
38
+
39
+ # --- 基础测试 ---
40
+
41
+ def test_simple_hex(self):
42
+ assert PARSE_HEX_BITS("AA BB CC") == ([0xAA, 0xBB, 0xCC], 0)
43
+
44
+ def test_empty_string(self):
45
+ assert PARSE_HEX_BITS("") == ([], 0)
46
+
47
+ def test_whitespace_only(self):
48
+ assert PARSE_HEX_BITS(" ") == ([], 0)
49
+
50
+ def test_invalid_hex_raises(self):
51
+ with pytest.raises(ValueError, match="不合法的十六进制数据"):
52
+ PARSE_HEX_BITS("ZZZZ")
53
+
54
+ # --- last_bits 参数测试 ---
55
+
56
+ def test_with_last_bits(self):
57
+ """last_bits=4 截断最后一个字节: 0x11 → 0x01"""
58
+ assert PARSE_HEX_BITS("00 11", last_bits=4) == ([0x00, 0x01], 4)
59
+
60
+ def test_with_last_bits_single_byte(self):
61
+ """last_bits=7 截断: 0xFF → 0x7F"""
62
+ assert PARSE_HEX_BITS("FF", last_bits=7) == ([0x7F], 7)
63
+
64
+ def test_with_last_bits_no_space(self):
65
+ assert PARSE_HEX_BITS("0011", last_bits=4) == ([0x00, 0x01], 4)
66
+
67
+ # --- Verilog 标记:无截断 ---
68
+
69
+ def test_verilog_no_truncate(self):
70
+ """值在 bits 范围内,无需截断"""
71
+ assert PARSE_HEX_BITS("7'h03") == ([0x03], 7)
72
+
73
+ def test_verilog_no_truncate_even(self):
74
+ """偶数位 hex,无需填充"""
75
+ assert PARSE_HEX_BITS("8'hFF") == ([0xFF], 8)
76
+
77
+ def test_verilog_no_truncate_4bits(self):
78
+ """4 位 hex 值,无需截断"""
79
+ assert PARSE_HEX_BITS("4'hA") == ([0x0A], 4)
80
+
81
+ def test_verilog_no_truncate_4bits_max(self):
82
+ """4 位最大值 F,无需截断"""
83
+ assert PARSE_HEX_BITS("4'hF") == ([0x0F], 4)
84
+
85
+ # --- Verilog 标记:截断高位 ---
86
+
87
+ def test_verilog_truncate_4bits(self):
88
+ """4'hCA → 截断高位 C,只保留 4 位 → 0x0A"""
89
+ assert PARSE_HEX_BITS("4'hCA") == ([0x0A], 4)
90
+
91
+ def test_verilog_truncate_7bits(self):
92
+ """7'hFF → FF 是 8 位,截断为 7 位 → 0x7F"""
93
+ assert PARSE_HEX_BITS("7'hFF") == ([0x7F], 7)
94
+
95
+ def test_verilog_truncate_4bits_bigger(self):
96
+ """4'hFF → 截断为 4 位 → 0x0F"""
97
+ assert PARSE_HEX_BITS("4'hFF") == ([0x0F], 4)
98
+
99
+ def test_verilog_truncate_1bit(self):
100
+ """1'h1 → 1 位最大值 1"""
101
+ assert PARSE_HEX_BITS("1'h1") == ([0x01], 1)
102
+
103
+ def test_verilog_truncate_1bit_zero(self):
104
+ """1'h0 → 1 位最小值 0"""
105
+ assert PARSE_HEX_BITS("1'h0") == ([0x00], 1)
106
+
107
+ # --- Verilog 标记:奇数位填充 ---
108
+
109
+ def test_verilog_odd_pad(self):
110
+ """4'hB → 奇数位填充为 0B"""
111
+ assert PARSE_HEX_BITS("4'hB") == ([0x0B], 4)
112
+
113
+ def test_verilog_odd_pad_7bits(self):
114
+ """7'hA → 奇数位填充为 0A,7 位无需截断"""
115
+ assert PARSE_HEX_BITS("7'hA") == ([0x0A], 7)
116
+
117
+ def test_verilog_odd_pad_truncate(self):
118
+ """4'hCB → 奇数位填充后截断 → 0x0B"""
119
+ assert PARSE_HEX_BITS("4'hCB") == ([0x0B], 4)
120
+
121
+ # --- 混合格式 ---
122
+
123
+ def test_mixed_hex_and_verilog(self):
124
+ """4'hBB → 截断为 4 位 → 0x0B"""
125
+ assert PARSE_HEX_BITS("AA 4'hBB") == ([0xAA, 0x0B], 4)
126
+
127
+ def test_mixed_verilog_last(self):
128
+ """Verilog 标记在最后"""
129
+ assert PARSE_HEX_BITS("AA BB 7'h03") == ([0xAA, 0xBB, 0x03], 7)
130
+
131
+ def test_mixed_no_space_verilog_end(self):
132
+ """AABB7'h26 → 正常解析"""
133
+ assert PARSE_HEX_BITS("AABB7'h26") == ([0xAA, 0xBB, 0x26], 7)
134
+
135
+ # --- 冲突检测 ---
136
+
137
+ def test_verilog_and_last_bits_conflict(self):
138
+ with pytest.raises(ValueError, match="冲突"):
139
+ PARSE_HEX_BITS("AA 4'hBB", last_bits=4)
140
+
141
+ def test_multiple_verilog_marks_error(self):
142
+ """多个 Verilog 标记报错"""
143
+ with pytest.raises(ValueError, match="只允许一个"):
144
+ PARSE_HEX_BITS("7'h03 AA 4'hC")
145
+
146
+ # --- 小写 hex ---
147
+
148
+ def test_verilog_lower_hex(self):
149
+ assert PARSE_HEX_BITS("7'h0a") == ([0x0A], 7)
150
+
151
+ def test_verilog_lower_hex_truncate(self):
152
+ """4'hca → 截断为 4 位 → 0x0A"""
153
+ assert PARSE_HEX_BITS("4'hca") == ([0x0A], 4)
154
+
155
+
156
+ class TestFORMAT_HEX:
157
+ """FORMAT_HEX: 格式化为可视化 hex"""
158
+
159
+ def test_int_no_bits(self):
160
+ assert FORMAT_HEX(0xAABBCC) == "AA BB CC"
161
+
162
+ def test_int_with_bits(self):
163
+ assert FORMAT_HEX(0xAABB03, 7) == "AA BB 7'h03"
164
+
165
+ def test_int_single_byte_bits(self):
166
+ assert FORMAT_HEX(0xFF, 4) == "4'hFF"
167
+
168
+ def test_bytes_no_bits(self):
169
+ assert FORMAT_HEX(b'\xAA\xBB\xCC') == "AA BB CC"
170
+
171
+ def test_bytes_with_bits(self):
172
+ assert FORMAT_HEX(b'\xAA\xBB\x03', 7) == "AA BB 7'h03"
173
+
174
+ def test_list_no_bits(self):
175
+ assert FORMAT_HEX([0xAA, 0xBB, 0xCC]) == "AA BB CC"
176
+
177
+ def test_list_with_bits(self):
178
+ assert FORMAT_HEX([0xAA, 0xBB, 0x03], 7) == "AA BB 7'h03"
179
+
180
+ def test_str_no_bits(self):
181
+ assert FORMAT_HEX("AABBCC") == "AA BB CC"
182
+
183
+ def test_str_with_bits(self):
184
+ assert FORMAT_HEX("AABB03", 7) == "AA BB 7'h03"
185
+
186
+ def test_empty_string(self):
187
+ assert FORMAT_HEX("") == ""
188
+
189
+ def test_empty_bytes(self):
190
+ assert FORMAT_HEX(b'') == ""
191
+
192
+ def test_empty_list(self):
193
+ assert FORMAT_HEX([]) == ""
194
+
195
+ def test_unsupported_type(self):
196
+ with pytest.raises(TypeError, match="不支持的输入类型"):
197
+ FORMAT_HEX(3.14)
198
+
199
+ def test_hex_str_with_spaces(self):
200
+ assert FORMAT_HEX("AA BB CC") == "AA BB CC"
201
+
202
+ def test_lowercase_int(self):
203
+ assert FORMAT_HEX(0xff) == "FF"
204
+
205
+ def test_int_zero(self):
206
+ assert FORMAT_HEX(0) == "00"
207
+
208
+ def test_bytes_single(self):
209
+ assert FORMAT_HEX(b'\xFF') == "FF"
210
+
211
+ def test_list_single(self):
212
+ assert FORMAT_HEX([0xFF]) == "FF"
213
+
214
+ def test_int_large(self):
215
+ assert FORMAT_HEX(0xAABBCCDD) == "AA BB CC DD"
216
+
217
+ def test_bits_1(self):
218
+ assert FORMAT_HEX(0x01, 1) == "1'h01"
219
+
220
+ def test_bits_7(self):
221
+ assert FORMAT_HEX(0x26, 7) == "7'h26"
222
+
223
+
224
+ class TestRoundtrip:
225
+ """PARSE_HEX <-> FORMAT_HEX 往返测试"""
226
+
227
+ def test_roundtrip_no_bits(self):
228
+ data = [0xAA, 0xBB, 0xCC]
229
+ formatted = FORMAT_HEX(data)
230
+ parsed = PARSE_HEX(formatted)
231
+ assert parsed == data
232
+
233
+ def test_roundtrip_with_bits(self):
234
+ data = [0xAA, 0xBB, 0x03]
235
+ formatted = FORMAT_HEX(data, 7)
236
+ parsed, bits = PARSE_HEX_BITS(formatted)
237
+ assert parsed == data
238
+ assert bits == 7
239
+
240
+ def test_roundtrip_single_byte(self):
241
+ """4'hFF 会被截断为 4 位 → 0x0F"""
242
+ formatted = FORMAT_HEX(0xFF, 4)
243
+ parsed, bits = PARSE_HEX_BITS(formatted)
244
+ assert parsed == [0x0F]
245
+ assert bits == 4
@@ -1,131 +0,0 @@
1
- import re
2
-
3
- # 位标记正则:匹配 N'hXX 格式 (如 7'h26, 4'hC)
4
- BIT_PATTERN = re.compile(r"(\d+)\s*\'h\s*([0-9a-fA-F]+)")
5
-
6
- def PARSE_HEX(text: str) -> tuple[str, int]:
7
- """
8
- 解析带位标记的 hex 字符串。
9
-
10
- Args:
11
- text: 十六进制文本。
12
-
13
- Returns:
14
- tuple[str, int]: (纯十六进制字符串, 最后字节有效位数)。
15
-
16
- Raises:
17
- ValueError: 不合法的十六进制数据时抛出。
18
-
19
- Examples:
20
- >>> PARSE_HEX("AA BB 7'h03")
21
- ('AABB03', 7)
22
- >>> PARSE_HEX("FF 4'h0C")
23
- ('FF0C', 4)
24
- >>> PARSE_HEX("AABBCC")
25
- ('AABBCC', 0)
26
- """
27
- last_bits = 0
28
- cleaned = text.strip()
29
-
30
- # 查找所有位标记,从右往左替换避免偏移
31
- matches = list(BIT_PATTERN.finditer(cleaned))
32
- if matches:
33
- last_match = matches[-1]
34
- last_bits = int(last_match.group(1))
35
-
36
- for m in reversed(matches):
37
- hex_val = m.group(2)
38
- if len(hex_val) % 2 != 0:
39
- hex_val = "0" + hex_val
40
- cleaned = cleaned[:m.start()] + hex_val + cleaned[m.end():]
41
-
42
- # 去空格得到纯 hex
43
- clean_hex = cleaned.replace(" ", "")
44
-
45
- # 简单的合法性检查
46
- if clean_hex:
47
- try:
48
- bytes.fromhex(clean_hex)
49
- except ValueError:
50
- raise ValueError(f"不合法的十六进制数据: {clean_hex}")
51
-
52
- return clean_hex, last_bits
53
-
54
- def FORMAT_HEX(data: int | bytes | list[int] | str, last_bits: int = 0) -> str:
55
- """
56
- 格式化数据为带位标记的可视化格式。
57
-
58
- 支持输入类型: int, bytes, list[int], str (纯 hex)。
59
-
60
- Args:
61
- data: 待格式化的数据。
62
- last_bits: 最后一个字节的有效位数 (1-7),0 表示整字节。
63
-
64
- Returns:
65
- str: 格式化后的字符串。
66
-
67
- Raises:
68
- TypeError: 不支持的输入类型时抛出。
69
-
70
- Examples:
71
- >>> FORMAT_HEX(0xAABB03, 7)
72
- "AA BB 7'h03"
73
- >>> FORMAT_HEX([0xAA, 0xBB, 0x03])
74
- 'AA BB 03'
75
- >>> FORMAT_HEX("AABB", 4)
76
- "AA 4'hBB"
77
- """
78
- # 1. 统一转换为纯十六进制字符串
79
- if isinstance(data, int):
80
- hex_str = data.to_bytes(max(1, (data.bit_length() + 7) // 8)).hex().upper()
81
- elif isinstance(data, bytes):
82
- hex_str = data.hex().upper()
83
- elif isinstance(data, list):
84
- hex_str = bytes(data).hex().upper()
85
- elif isinstance(data, str):
86
- hex_str = data.replace(" ", "").upper()
87
- else:
88
- raise TypeError(f"不支持的输入类型: {type(data)}")
89
-
90
- if not hex_str:
91
- return ""
92
-
93
- # 2. 如果是整字节 (无 last_bits)
94
- if last_bits == 0:
95
- return ' '.join(hex_str[i:i+2] for i in range(0, len(hex_str), 2))
96
-
97
- # 3. 有 last_bits,处理最后一个字节
98
- if len(hex_str) <= 2:
99
- return f"{last_bits}'h{hex_str}"
100
-
101
- prefix = hex_str[:-2]
102
- last_byte = hex_str[-2:]
103
-
104
- formatted_prefix = ' '.join(prefix[i:i+2] for i in range(0, len(prefix), 2))
105
- return f"{formatted_prefix} {last_bits}'h{last_byte}"
106
-
107
- if __name__ == "__main__":
108
- # --- 测试例程 ---
109
- print("=== Hex 工具测试 ===")
110
-
111
- # 测试用例: (data, last_bits, 类型名)
112
- test_cases = [
113
- ("AABB03", 7, "str"),
114
- (0xAABB03, 7, "int"),
115
- (b'\xAA\xBB\x03', 7, "bytes"),
116
- ([0xAA, 0xBB, 0x03], 7, "list"),
117
- ("FF", 4, "str"),
118
- (0xFF, 4, "int"),
119
- ("AABBCC", 0, "str"),
120
- (0xAABBCC, 0, "int"),
121
- (b'\xAA\xBB\xCC', 0, "bytes"),
122
- ]
123
-
124
- for data, b, typename in test_cases:
125
- formatted = FORMAT_HEX(data, b)
126
- print(f"输入: ({data}, {b}, {typename}) -> 格式化: {formatted}")
127
-
128
- # 反向解析验证
129
- parsed_h, parsed_b = PARSE_HEX(formatted)
130
- print(f"解析: {formatted} -> Hex: {parsed_h}, Bits: {parsed_b}")
131
- print("-" * 20)
@@ -1,168 +0,0 @@
1
- """PARSE_HEX / FORMAT_HEX 测试"""
2
- import pytest
3
- from nfc.hex_util import PARSE_HEX, FORMAT_HEX
4
-
5
-
6
- class TestPARSE_HEX:
7
-
8
- def test_simple_hex(self):
9
- assert PARSE_HEX("AA BB CC") == ("AABBCC", 0)
10
-
11
- def test_bit_mark(self):
12
- hex_str, bits = PARSE_HEX("AA BB 7'h03")
13
- assert hex_str == "AABB03"
14
- assert bits == 7
15
-
16
- def test_single_byte_two_digits(self):
17
- hex_str, bits = PARSE_HEX("4'h0C")
18
- assert hex_str == "0C"
19
- assert bits == 4
20
-
21
- def test_single_byte_odd_hex_padded(self):
22
- hex_str, bits = PARSE_HEX("4'hC")
23
- assert hex_str == "0C"
24
- assert bits == 4
25
-
26
- def test_single_byte_odd_hex_a(self):
27
- hex_str, bits = PARSE_HEX("7'h3")
28
- assert hex_str == "03"
29
- assert bits == 7
30
-
31
- def test_no_space(self):
32
- assert PARSE_HEX("AABBCC") == ("AABBCC", 0)
33
-
34
- def test_empty_string(self):
35
- assert PARSE_HEX("") == ("", 0)
36
-
37
- def test_whitespace_only(self):
38
- assert PARSE_HEX(" ") == ("", 0)
39
-
40
- def test_multiple_bit_marks_all_replaced(self):
41
- hex_str, bits = PARSE_HEX("7'h03 AA 4'hC")
42
- assert hex_str == "03AA0C"
43
- assert bits == 4
44
-
45
- def test_multiple_bit_marks_first_and_last(self):
46
- hex_str, bits = PARSE_HEX("4'hA BB 7'h03")
47
- assert hex_str == "0ABB03"
48
- assert bits == 7
49
-
50
- def test_three_bit_marks(self):
51
- hex_str, bits = PARSE_HEX("4'h1 4'h2 4'h3")
52
- assert hex_str == "010203"
53
- assert bits == 4
54
-
55
- def test_invalid_hex_raises(self):
56
- with pytest.raises(ValueError, match="不合法的十六进制数据"):
57
- PARSE_HEX("ZZZZ")
58
-
59
- def test_odd_hex_digits_raises(self):
60
- with pytest.raises(ValueError):
61
- PARSE_HEX("ABC")
62
-
63
- def test_hex_with_uppercase(self):
64
- assert PARSE_HEX("FF AA") == ("FFAA", 0)
65
-
66
- def test_hex_with_lowercase(self):
67
- assert PARSE_HEX("ff aa") == ("ffaa", 0)
68
-
69
- def test_bit_mark_no_space_around(self):
70
- hex_str, bits = PARSE_HEX("AA7'h03BB")
71
- assert hex_str == "AA03BB"
72
- assert bits == 7
73
-
74
- def test_bit_mark_only(self):
75
- hex_str, bits = PARSE_HEX("8'hFF")
76
- assert hex_str == "FF"
77
- assert bits == 8
78
-
79
- def test_bit_mark_with_lower_hex(self):
80
- hex_str, bits = PARSE_HEX("7'h0a")
81
- assert hex_str == "0a"
82
- assert bits == 7
83
-
84
- def test_roundtrip_with_format(self):
85
- data = [0xAA, 0xBB, 0x03]
86
- formatted = FORMAT_HEX(data, 7)
87
- parsed, bits = PARSE_HEX(formatted)
88
- assert bits == 7
89
- assert bytes.fromhex(parsed) == bytes(data)
90
-
91
- def test_roundtrip_single_byte(self):
92
- formatted = FORMAT_HEX(0xFF, 4)
93
- parsed, bits = PARSE_HEX(formatted)
94
- assert bits == 4
95
- assert parsed == "FF"
96
-
97
- def test_roundtrip_no_bits(self):
98
- formatted = FORMAT_HEX([0x01, 0x02, 0x03])
99
- parsed, bits = PARSE_HEX(formatted)
100
- assert bits == 0
101
- assert parsed == "010203"
102
-
103
-
104
- class TestFORMAT_HEX:
105
-
106
- def test_int_no_bits(self):
107
- assert FORMAT_HEX(0xAABBCC) == "AA BB CC"
108
-
109
- def test_int_with_bits(self):
110
- assert FORMAT_HEX(0xAABB03, 7) == "AA BB 7'h03"
111
-
112
- def test_int_single_byte_bits(self):
113
- assert FORMAT_HEX(0xFF, 4) == "4'hFF"
114
-
115
- def test_bytes_no_bits(self):
116
- assert FORMAT_HEX(b'\xAA\xBB\xCC') == "AA BB CC"
117
-
118
- def test_bytes_with_bits(self):
119
- assert FORMAT_HEX(b'\xAA\xBB\x03', 7) == "AA BB 7'h03"
120
-
121
- def test_list_no_bits(self):
122
- assert FORMAT_HEX([0xAA, 0xBB, 0xCC]) == "AA BB CC"
123
-
124
- def test_list_with_bits(self):
125
- assert FORMAT_HEX([0xAA, 0xBB, 0x03], 7) == "AA BB 7'h03"
126
-
127
- def test_str_no_bits(self):
128
- assert FORMAT_HEX("AABBCC") == "AA BB CC"
129
-
130
- def test_str_with_bits(self):
131
- assert FORMAT_HEX("AABB03", 7) == "AA BB 7'h03"
132
-
133
- def test_empty_string(self):
134
- assert FORMAT_HEX("") == ""
135
-
136
- def test_empty_bytes(self):
137
- assert FORMAT_HEX(b'') == ""
138
-
139
- def test_empty_list(self):
140
- assert FORMAT_HEX([]) == ""
141
-
142
- def test_unsupported_type(self):
143
- with pytest.raises(TypeError, match="不支持的输入类型"):
144
- FORMAT_HEX(3.14)
145
-
146
- def test_hex_str_with_spaces(self):
147
- assert FORMAT_HEX("AA BB CC") == "AA BB CC"
148
-
149
- def test_lowercase_int(self):
150
- assert FORMAT_HEX(0xff) == "FF"
151
-
152
- def test_int_zero(self):
153
- assert FORMAT_HEX(0) == "00"
154
-
155
- def test_bytes_single(self):
156
- assert FORMAT_HEX(b'\xFF') == "FF"
157
-
158
- def test_list_single(self):
159
- assert FORMAT_HEX([0xFF]) == "FF"
160
-
161
- def test_int_large(self):
162
- assert FORMAT_HEX(0xAABBCCDD) == "AA BB CC DD"
163
-
164
- def test_bits_1(self):
165
- assert FORMAT_HEX(0x01, 1) == "1'h01"
166
-
167
- def test_bits_7(self):
168
- assert FORMAT_HEX(0x26, 7) == "7'h26"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes