oscura 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

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 (513) hide show
  1. oscura/__init__.py +169 -167
  2. oscura/analyzers/__init__.py +3 -0
  3. oscura/analyzers/classification.py +659 -0
  4. oscura/analyzers/digital/__init__.py +0 -48
  5. oscura/analyzers/digital/edges.py +325 -65
  6. oscura/analyzers/digital/extraction.py +0 -195
  7. oscura/analyzers/digital/quality.py +293 -166
  8. oscura/analyzers/digital/timing.py +260 -115
  9. oscura/analyzers/digital/timing_numba.py +334 -0
  10. oscura/analyzers/entropy.py +605 -0
  11. oscura/analyzers/eye/diagram.py +176 -109
  12. oscura/analyzers/eye/metrics.py +5 -5
  13. oscura/analyzers/jitter/__init__.py +6 -4
  14. oscura/analyzers/jitter/ber.py +52 -52
  15. oscura/analyzers/jitter/classification.py +156 -0
  16. oscura/analyzers/jitter/decomposition.py +163 -113
  17. oscura/analyzers/jitter/spectrum.py +80 -64
  18. oscura/analyzers/ml/__init__.py +39 -0
  19. oscura/analyzers/ml/features.py +600 -0
  20. oscura/analyzers/ml/signal_classifier.py +604 -0
  21. oscura/analyzers/packet/daq.py +246 -158
  22. oscura/analyzers/packet/parser.py +12 -1
  23. oscura/analyzers/packet/payload.py +50 -2110
  24. oscura/analyzers/packet/payload_analysis.py +361 -181
  25. oscura/analyzers/packet/payload_patterns.py +133 -70
  26. oscura/analyzers/packet/stream.py +84 -23
  27. oscura/analyzers/patterns/__init__.py +26 -5
  28. oscura/analyzers/patterns/anomaly_detection.py +908 -0
  29. oscura/analyzers/patterns/clustering.py +169 -108
  30. oscura/analyzers/patterns/clustering_optimized.py +227 -0
  31. oscura/analyzers/patterns/discovery.py +1 -1
  32. oscura/analyzers/patterns/matching.py +581 -197
  33. oscura/analyzers/patterns/pattern_mining.py +778 -0
  34. oscura/analyzers/patterns/periodic.py +121 -38
  35. oscura/analyzers/patterns/sequences.py +175 -78
  36. oscura/analyzers/power/conduction.py +1 -1
  37. oscura/analyzers/power/soa.py +6 -6
  38. oscura/analyzers/power/switching.py +250 -110
  39. oscura/analyzers/protocol/__init__.py +17 -1
  40. oscura/analyzers/protocols/__init__.py +1 -22
  41. oscura/analyzers/protocols/base.py +6 -6
  42. oscura/analyzers/protocols/ble/__init__.py +38 -0
  43. oscura/analyzers/protocols/ble/analyzer.py +809 -0
  44. oscura/analyzers/protocols/ble/uuids.py +288 -0
  45. oscura/analyzers/protocols/can.py +257 -127
  46. oscura/analyzers/protocols/can_fd.py +107 -80
  47. oscura/analyzers/protocols/flexray.py +139 -80
  48. oscura/analyzers/protocols/hdlc.py +93 -58
  49. oscura/analyzers/protocols/i2c.py +247 -106
  50. oscura/analyzers/protocols/i2s.py +138 -86
  51. oscura/analyzers/protocols/industrial/__init__.py +40 -0
  52. oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
  53. oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
  54. oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
  55. oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
  56. oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
  57. oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
  58. oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
  59. oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
  60. oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
  61. oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
  62. oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
  63. oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
  64. oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
  65. oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
  66. oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
  67. oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
  68. oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
  69. oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
  70. oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
  71. oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
  72. oscura/analyzers/protocols/jtag.py +180 -98
  73. oscura/analyzers/protocols/lin.py +219 -114
  74. oscura/analyzers/protocols/manchester.py +4 -4
  75. oscura/analyzers/protocols/onewire.py +253 -149
  76. oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
  77. oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
  78. oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
  79. oscura/analyzers/protocols/spi.py +192 -95
  80. oscura/analyzers/protocols/swd.py +321 -167
  81. oscura/analyzers/protocols/uart.py +267 -125
  82. oscura/analyzers/protocols/usb.py +235 -131
  83. oscura/analyzers/side_channel/power.py +17 -12
  84. oscura/analyzers/signal/__init__.py +15 -0
  85. oscura/analyzers/signal/timing_analysis.py +1086 -0
  86. oscura/analyzers/signal_integrity/__init__.py +4 -1
  87. oscura/analyzers/signal_integrity/sparams.py +2 -19
  88. oscura/analyzers/spectral/chunked.py +129 -60
  89. oscura/analyzers/spectral/chunked_fft.py +300 -94
  90. oscura/analyzers/spectral/chunked_wavelet.py +100 -80
  91. oscura/analyzers/statistical/checksum.py +376 -217
  92. oscura/analyzers/statistical/classification.py +229 -107
  93. oscura/analyzers/statistical/entropy.py +78 -53
  94. oscura/analyzers/statistics/correlation.py +407 -211
  95. oscura/analyzers/statistics/outliers.py +2 -2
  96. oscura/analyzers/statistics/streaming.py +30 -5
  97. oscura/analyzers/validation.py +216 -101
  98. oscura/analyzers/waveform/measurements.py +9 -0
  99. oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
  100. oscura/analyzers/waveform/spectral.py +500 -228
  101. oscura/api/__init__.py +31 -5
  102. oscura/api/dsl/__init__.py +582 -0
  103. oscura/{dsl → api/dsl}/commands.py +43 -76
  104. oscura/{dsl → api/dsl}/interpreter.py +26 -51
  105. oscura/{dsl → api/dsl}/parser.py +107 -77
  106. oscura/{dsl → api/dsl}/repl.py +2 -2
  107. oscura/api/dsl.py +1 -1
  108. oscura/{integrations → api/integrations}/__init__.py +1 -1
  109. oscura/{integrations → api/integrations}/llm.py +201 -102
  110. oscura/api/operators.py +3 -3
  111. oscura/api/optimization.py +144 -30
  112. oscura/api/rest_server.py +921 -0
  113. oscura/api/server/__init__.py +17 -0
  114. oscura/api/server/dashboard.py +850 -0
  115. oscura/api/server/static/README.md +34 -0
  116. oscura/api/server/templates/base.html +181 -0
  117. oscura/api/server/templates/export.html +120 -0
  118. oscura/api/server/templates/home.html +284 -0
  119. oscura/api/server/templates/protocols.html +58 -0
  120. oscura/api/server/templates/reports.html +43 -0
  121. oscura/api/server/templates/session_detail.html +89 -0
  122. oscura/api/server/templates/sessions.html +83 -0
  123. oscura/api/server/templates/waveforms.html +73 -0
  124. oscura/automotive/__init__.py +8 -1
  125. oscura/automotive/can/__init__.py +10 -0
  126. oscura/automotive/can/checksum.py +3 -1
  127. oscura/automotive/can/dbc_generator.py +590 -0
  128. oscura/automotive/can/message_wrapper.py +121 -74
  129. oscura/automotive/can/patterns.py +98 -21
  130. oscura/automotive/can/session.py +292 -56
  131. oscura/automotive/can/state_machine.py +6 -3
  132. oscura/automotive/can/stimulus_response.py +97 -75
  133. oscura/automotive/dbc/__init__.py +10 -2
  134. oscura/automotive/dbc/generator.py +84 -56
  135. oscura/automotive/dbc/parser.py +6 -6
  136. oscura/automotive/dtc/data.json +2763 -0
  137. oscura/automotive/dtc/database.py +2 -2
  138. oscura/automotive/flexray/__init__.py +31 -0
  139. oscura/automotive/flexray/analyzer.py +504 -0
  140. oscura/automotive/flexray/crc.py +185 -0
  141. oscura/automotive/flexray/fibex.py +449 -0
  142. oscura/automotive/j1939/__init__.py +45 -8
  143. oscura/automotive/j1939/analyzer.py +605 -0
  144. oscura/automotive/j1939/spns.py +326 -0
  145. oscura/automotive/j1939/transport.py +306 -0
  146. oscura/automotive/lin/__init__.py +47 -0
  147. oscura/automotive/lin/analyzer.py +612 -0
  148. oscura/automotive/loaders/blf.py +13 -2
  149. oscura/automotive/loaders/csv_can.py +143 -72
  150. oscura/automotive/loaders/dispatcher.py +50 -2
  151. oscura/automotive/loaders/mdf.py +86 -45
  152. oscura/automotive/loaders/pcap.py +111 -61
  153. oscura/automotive/uds/__init__.py +4 -0
  154. oscura/automotive/uds/analyzer.py +725 -0
  155. oscura/automotive/uds/decoder.py +140 -58
  156. oscura/automotive/uds/models.py +7 -1
  157. oscura/automotive/visualization.py +1 -1
  158. oscura/cli/analyze.py +348 -0
  159. oscura/cli/batch.py +142 -122
  160. oscura/cli/benchmark.py +275 -0
  161. oscura/cli/characterize.py +137 -82
  162. oscura/cli/compare.py +224 -131
  163. oscura/cli/completion.py +250 -0
  164. oscura/cli/config_cmd.py +361 -0
  165. oscura/cli/decode.py +164 -87
  166. oscura/cli/export.py +286 -0
  167. oscura/cli/main.py +115 -31
  168. oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
  169. oscura/{onboarding → cli/onboarding}/help.py +80 -58
  170. oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
  171. oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
  172. oscura/cli/progress.py +147 -0
  173. oscura/cli/shell.py +157 -135
  174. oscura/cli/validate_cmd.py +204 -0
  175. oscura/cli/visualize.py +158 -0
  176. oscura/convenience.py +125 -79
  177. oscura/core/__init__.py +4 -2
  178. oscura/core/backend_selector.py +3 -3
  179. oscura/core/cache.py +126 -15
  180. oscura/core/cancellation.py +1 -1
  181. oscura/{config → core/config}/__init__.py +20 -11
  182. oscura/{config → core/config}/defaults.py +1 -1
  183. oscura/{config → core/config}/loader.py +7 -5
  184. oscura/{config → core/config}/memory.py +5 -5
  185. oscura/{config → core/config}/migration.py +1 -1
  186. oscura/{config → core/config}/pipeline.py +99 -23
  187. oscura/{config → core/config}/preferences.py +1 -1
  188. oscura/{config → core/config}/protocol.py +3 -3
  189. oscura/{config → core/config}/schema.py +426 -272
  190. oscura/{config → core/config}/settings.py +1 -1
  191. oscura/{config → core/config}/thresholds.py +195 -153
  192. oscura/core/correlation.py +5 -6
  193. oscura/core/cross_domain.py +0 -2
  194. oscura/core/debug.py +9 -5
  195. oscura/{extensibility → core/extensibility}/docs.py +158 -70
  196. oscura/{extensibility → core/extensibility}/extensions.py +160 -76
  197. oscura/{extensibility → core/extensibility}/logging.py +1 -1
  198. oscura/{extensibility → core/extensibility}/measurements.py +1 -1
  199. oscura/{extensibility → core/extensibility}/plugins.py +1 -1
  200. oscura/{extensibility → core/extensibility}/templates.py +73 -3
  201. oscura/{extensibility → core/extensibility}/validation.py +1 -1
  202. oscura/core/gpu_backend.py +11 -7
  203. oscura/core/log_query.py +101 -11
  204. oscura/core/logging.py +126 -54
  205. oscura/core/logging_advanced.py +5 -5
  206. oscura/core/memory_limits.py +108 -70
  207. oscura/core/memory_monitor.py +2 -2
  208. oscura/core/memory_progress.py +7 -7
  209. oscura/core/memory_warnings.py +1 -1
  210. oscura/core/numba_backend.py +13 -13
  211. oscura/{plugins → core/plugins}/__init__.py +9 -9
  212. oscura/{plugins → core/plugins}/base.py +7 -7
  213. oscura/{plugins → core/plugins}/cli.py +3 -3
  214. oscura/{plugins → core/plugins}/discovery.py +186 -106
  215. oscura/{plugins → core/plugins}/lifecycle.py +1 -1
  216. oscura/{plugins → core/plugins}/manager.py +7 -7
  217. oscura/{plugins → core/plugins}/registry.py +3 -3
  218. oscura/{plugins → core/plugins}/versioning.py +1 -1
  219. oscura/core/progress.py +16 -1
  220. oscura/core/provenance.py +8 -2
  221. oscura/{schemas → core/schemas}/__init__.py +2 -2
  222. oscura/core/schemas/bus_configuration.json +322 -0
  223. oscura/core/schemas/device_mapping.json +182 -0
  224. oscura/core/schemas/packet_format.json +418 -0
  225. oscura/core/schemas/protocol_definition.json +363 -0
  226. oscura/core/types.py +4 -0
  227. oscura/core/uncertainty.py +3 -3
  228. oscura/correlation/__init__.py +52 -0
  229. oscura/correlation/multi_protocol.py +811 -0
  230. oscura/discovery/auto_decoder.py +117 -35
  231. oscura/discovery/comparison.py +191 -86
  232. oscura/discovery/quality_validator.py +155 -68
  233. oscura/discovery/signal_detector.py +196 -79
  234. oscura/export/__init__.py +18 -20
  235. oscura/export/kaitai_struct.py +513 -0
  236. oscura/export/scapy_layer.py +801 -0
  237. oscura/export/wireshark/README.md +15 -15
  238. oscura/export/wireshark/generator.py +1 -1
  239. oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
  240. oscura/export/wireshark_dissector.py +746 -0
  241. oscura/guidance/wizard.py +207 -111
  242. oscura/hardware/__init__.py +19 -0
  243. oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
  244. oscura/{acquisition → hardware/acquisition}/file.py +2 -2
  245. oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
  246. oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
  247. oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
  248. oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
  249. oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
  250. oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
  251. oscura/hardware/firmware/__init__.py +29 -0
  252. oscura/hardware/firmware/pattern_recognition.py +874 -0
  253. oscura/hardware/hal_detector.py +736 -0
  254. oscura/hardware/security/__init__.py +37 -0
  255. oscura/hardware/security/side_channel_detector.py +1126 -0
  256. oscura/inference/__init__.py +4 -0
  257. oscura/inference/active_learning/README.md +7 -7
  258. oscura/inference/active_learning/observation_table.py +4 -1
  259. oscura/inference/alignment.py +216 -123
  260. oscura/inference/bayesian.py +113 -33
  261. oscura/inference/crc_reverse.py +101 -55
  262. oscura/inference/logic.py +6 -2
  263. oscura/inference/message_format.py +342 -183
  264. oscura/inference/protocol.py +95 -44
  265. oscura/inference/protocol_dsl.py +180 -82
  266. oscura/inference/signal_intelligence.py +1439 -706
  267. oscura/inference/spectral.py +99 -57
  268. oscura/inference/state_machine.py +810 -158
  269. oscura/inference/stream.py +270 -110
  270. oscura/iot/__init__.py +34 -0
  271. oscura/iot/coap/__init__.py +32 -0
  272. oscura/iot/coap/analyzer.py +668 -0
  273. oscura/iot/coap/options.py +212 -0
  274. oscura/iot/lorawan/__init__.py +21 -0
  275. oscura/iot/lorawan/crypto.py +206 -0
  276. oscura/iot/lorawan/decoder.py +801 -0
  277. oscura/iot/lorawan/mac_commands.py +341 -0
  278. oscura/iot/mqtt/__init__.py +27 -0
  279. oscura/iot/mqtt/analyzer.py +999 -0
  280. oscura/iot/mqtt/properties.py +315 -0
  281. oscura/iot/zigbee/__init__.py +31 -0
  282. oscura/iot/zigbee/analyzer.py +615 -0
  283. oscura/iot/zigbee/security.py +153 -0
  284. oscura/iot/zigbee/zcl.py +349 -0
  285. oscura/jupyter/display.py +125 -45
  286. oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
  287. oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
  288. oscura/jupyter/exploratory/fuzzy.py +746 -0
  289. oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
  290. oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
  291. oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
  292. oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
  293. oscura/jupyter/exploratory/sync.py +612 -0
  294. oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
  295. oscura/jupyter/magic.py +4 -4
  296. oscura/{ui → jupyter/ui}/__init__.py +2 -2
  297. oscura/{ui → jupyter/ui}/formatters.py +3 -3
  298. oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
  299. oscura/loaders/__init__.py +171 -63
  300. oscura/loaders/binary.py +88 -1
  301. oscura/loaders/chipwhisperer.py +153 -137
  302. oscura/loaders/configurable.py +208 -86
  303. oscura/loaders/csv_loader.py +458 -215
  304. oscura/loaders/hdf5_loader.py +278 -119
  305. oscura/loaders/lazy.py +87 -54
  306. oscura/loaders/mmap_loader.py +1 -1
  307. oscura/loaders/numpy_loader.py +253 -116
  308. oscura/loaders/pcap.py +226 -151
  309. oscura/loaders/rigol.py +110 -49
  310. oscura/loaders/sigrok.py +201 -78
  311. oscura/loaders/tdms.py +81 -58
  312. oscura/loaders/tektronix.py +291 -174
  313. oscura/loaders/touchstone.py +182 -87
  314. oscura/loaders/vcd.py +215 -117
  315. oscura/loaders/wav.py +155 -68
  316. oscura/reporting/__init__.py +9 -7
  317. oscura/reporting/analyze.py +352 -146
  318. oscura/reporting/argument_preparer.py +69 -14
  319. oscura/reporting/auto_report.py +97 -61
  320. oscura/reporting/batch.py +131 -58
  321. oscura/reporting/chart_selection.py +57 -45
  322. oscura/reporting/comparison.py +63 -17
  323. oscura/reporting/content/executive.py +76 -24
  324. oscura/reporting/core_formats/multi_format.py +11 -8
  325. oscura/reporting/engine.py +312 -158
  326. oscura/reporting/enhanced_reports.py +949 -0
  327. oscura/reporting/export.py +86 -43
  328. oscura/reporting/formatting/numbers.py +69 -42
  329. oscura/reporting/html.py +139 -58
  330. oscura/reporting/index.py +137 -65
  331. oscura/reporting/output.py +158 -67
  332. oscura/reporting/pdf.py +67 -102
  333. oscura/reporting/plots.py +191 -112
  334. oscura/reporting/sections.py +88 -47
  335. oscura/reporting/standards.py +104 -61
  336. oscura/reporting/summary_generator.py +75 -55
  337. oscura/reporting/tables.py +138 -54
  338. oscura/reporting/templates/enhanced/protocol_re.html +525 -0
  339. oscura/reporting/templates/index.md +13 -13
  340. oscura/sessions/__init__.py +14 -23
  341. oscura/sessions/base.py +3 -3
  342. oscura/sessions/blackbox.py +106 -10
  343. oscura/sessions/generic.py +2 -2
  344. oscura/sessions/legacy.py +783 -0
  345. oscura/side_channel/__init__.py +63 -0
  346. oscura/side_channel/dpa.py +1025 -0
  347. oscura/utils/__init__.py +15 -1
  348. oscura/utils/autodetect.py +1 -5
  349. oscura/utils/bitwise.py +118 -0
  350. oscura/{builders → utils/builders}/__init__.py +1 -1
  351. oscura/{comparison → utils/comparison}/__init__.py +6 -6
  352. oscura/{comparison → utils/comparison}/compare.py +202 -101
  353. oscura/{comparison → utils/comparison}/golden.py +83 -63
  354. oscura/{comparison → utils/comparison}/limits.py +313 -89
  355. oscura/{comparison → utils/comparison}/mask.py +151 -45
  356. oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
  357. oscura/{comparison → utils/comparison}/visualization.py +147 -89
  358. oscura/{component → utils/component}/__init__.py +3 -3
  359. oscura/{component → utils/component}/impedance.py +122 -58
  360. oscura/{component → utils/component}/reactive.py +165 -168
  361. oscura/{component → utils/component}/transmission_line.py +3 -3
  362. oscura/{filtering → utils/filtering}/__init__.py +6 -6
  363. oscura/{filtering → utils/filtering}/base.py +1 -1
  364. oscura/{filtering → utils/filtering}/convenience.py +2 -2
  365. oscura/{filtering → utils/filtering}/design.py +169 -93
  366. oscura/{filtering → utils/filtering}/filters.py +2 -2
  367. oscura/{filtering → utils/filtering}/introspection.py +2 -2
  368. oscura/utils/geometry.py +31 -0
  369. oscura/utils/imports.py +184 -0
  370. oscura/utils/lazy.py +1 -1
  371. oscura/{math → utils/math}/__init__.py +2 -2
  372. oscura/{math → utils/math}/arithmetic.py +114 -48
  373. oscura/{math → utils/math}/interpolation.py +139 -106
  374. oscura/utils/memory.py +129 -66
  375. oscura/utils/memory_advanced.py +92 -9
  376. oscura/utils/memory_extensions.py +10 -8
  377. oscura/{optimization → utils/optimization}/__init__.py +1 -1
  378. oscura/{optimization → utils/optimization}/search.py +2 -2
  379. oscura/utils/performance/__init__.py +58 -0
  380. oscura/utils/performance/caching.py +889 -0
  381. oscura/utils/performance/lsh_clustering.py +333 -0
  382. oscura/utils/performance/memory_optimizer.py +699 -0
  383. oscura/utils/performance/optimizations.py +675 -0
  384. oscura/utils/performance/parallel.py +654 -0
  385. oscura/utils/performance/profiling.py +661 -0
  386. oscura/{pipeline → utils/pipeline}/base.py +1 -1
  387. oscura/{pipeline → utils/pipeline}/composition.py +11 -3
  388. oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
  389. oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
  390. oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
  391. oscura/{search → utils/search}/__init__.py +3 -3
  392. oscura/{search → utils/search}/anomaly.py +188 -58
  393. oscura/utils/search/context.py +294 -0
  394. oscura/{search → utils/search}/pattern.py +138 -10
  395. oscura/utils/serial.py +51 -0
  396. oscura/utils/storage/__init__.py +61 -0
  397. oscura/utils/storage/database.py +1166 -0
  398. oscura/{streaming → utils/streaming}/chunked.py +302 -143
  399. oscura/{streaming → utils/streaming}/progressive.py +1 -1
  400. oscura/{streaming → utils/streaming}/realtime.py +3 -2
  401. oscura/{triggering → utils/triggering}/__init__.py +6 -6
  402. oscura/{triggering → utils/triggering}/base.py +6 -6
  403. oscura/{triggering → utils/triggering}/edge.py +2 -2
  404. oscura/{triggering → utils/triggering}/pattern.py +2 -2
  405. oscura/{triggering → utils/triggering}/pulse.py +115 -74
  406. oscura/{triggering → utils/triggering}/window.py +2 -2
  407. oscura/utils/validation.py +32 -0
  408. oscura/validation/__init__.py +121 -0
  409. oscura/{compliance → validation/compliance}/__init__.py +5 -5
  410. oscura/{compliance → validation/compliance}/advanced.py +5 -5
  411. oscura/{compliance → validation/compliance}/masks.py +1 -1
  412. oscura/{compliance → validation/compliance}/reporting.py +127 -53
  413. oscura/{compliance → validation/compliance}/testing.py +114 -52
  414. oscura/validation/compliance_tests.py +915 -0
  415. oscura/validation/fuzzer.py +990 -0
  416. oscura/validation/grammar_tests.py +596 -0
  417. oscura/validation/grammar_validator.py +904 -0
  418. oscura/validation/hil_testing.py +977 -0
  419. oscura/{quality → validation/quality}/__init__.py +4 -4
  420. oscura/{quality → validation/quality}/ensemble.py +251 -171
  421. oscura/{quality → validation/quality}/explainer.py +3 -3
  422. oscura/{quality → validation/quality}/scoring.py +1 -1
  423. oscura/{quality → validation/quality}/warnings.py +4 -4
  424. oscura/validation/regression_suite.py +808 -0
  425. oscura/validation/replay.py +788 -0
  426. oscura/{testing → validation/testing}/__init__.py +2 -2
  427. oscura/{testing → validation/testing}/synthetic.py +5 -5
  428. oscura/visualization/__init__.py +9 -0
  429. oscura/visualization/accessibility.py +1 -1
  430. oscura/visualization/annotations.py +64 -67
  431. oscura/visualization/colors.py +7 -7
  432. oscura/visualization/digital.py +180 -81
  433. oscura/visualization/eye.py +236 -85
  434. oscura/visualization/interactive.py +320 -143
  435. oscura/visualization/jitter.py +587 -247
  436. oscura/visualization/layout.py +169 -134
  437. oscura/visualization/optimization.py +103 -52
  438. oscura/visualization/palettes.py +1 -1
  439. oscura/visualization/power.py +427 -211
  440. oscura/visualization/power_extended.py +626 -297
  441. oscura/visualization/presets.py +2 -0
  442. oscura/visualization/protocols.py +495 -181
  443. oscura/visualization/render.py +79 -63
  444. oscura/visualization/reverse_engineering.py +171 -124
  445. oscura/visualization/signal_integrity.py +460 -279
  446. oscura/visualization/specialized.py +190 -100
  447. oscura/visualization/spectral.py +670 -255
  448. oscura/visualization/thumbnails.py +166 -137
  449. oscura/visualization/waveform.py +150 -63
  450. oscura/workflows/__init__.py +3 -0
  451. oscura/{batch → workflows/batch}/__init__.py +5 -5
  452. oscura/{batch → workflows/batch}/advanced.py +150 -75
  453. oscura/workflows/batch/aggregate.py +531 -0
  454. oscura/workflows/batch/analyze.py +236 -0
  455. oscura/{batch → workflows/batch}/logging.py +2 -2
  456. oscura/{batch → workflows/batch}/metrics.py +1 -1
  457. oscura/workflows/complete_re.py +1144 -0
  458. oscura/workflows/compliance.py +44 -54
  459. oscura/workflows/digital.py +197 -51
  460. oscura/workflows/legacy/__init__.py +12 -0
  461. oscura/{workflow → workflows/legacy}/dag.py +4 -1
  462. oscura/workflows/multi_trace.py +9 -9
  463. oscura/workflows/power.py +42 -62
  464. oscura/workflows/protocol.py +82 -49
  465. oscura/workflows/reverse_engineering.py +351 -150
  466. oscura/workflows/signal_integrity.py +157 -82
  467. oscura-0.6.0.dist-info/METADATA +643 -0
  468. oscura-0.6.0.dist-info/RECORD +590 -0
  469. oscura/analyzers/digital/ic_database.py +0 -498
  470. oscura/analyzers/digital/timing_paths.py +0 -339
  471. oscura/analyzers/digital/vintage.py +0 -377
  472. oscura/analyzers/digital/vintage_result.py +0 -148
  473. oscura/analyzers/protocols/parallel_bus.py +0 -449
  474. oscura/batch/aggregate.py +0 -300
  475. oscura/batch/analyze.py +0 -139
  476. oscura/dsl/__init__.py +0 -73
  477. oscura/exceptions.py +0 -59
  478. oscura/exploratory/fuzzy.py +0 -513
  479. oscura/exploratory/sync.py +0 -384
  480. oscura/export/wavedrom.py +0 -430
  481. oscura/exporters/__init__.py +0 -94
  482. oscura/exporters/csv.py +0 -303
  483. oscura/exporters/exporters.py +0 -44
  484. oscura/exporters/hdf5.py +0 -217
  485. oscura/exporters/html_export.py +0 -701
  486. oscura/exporters/json_export.py +0 -338
  487. oscura/exporters/markdown_export.py +0 -367
  488. oscura/exporters/matlab_export.py +0 -354
  489. oscura/exporters/npz_export.py +0 -219
  490. oscura/exporters/spice_export.py +0 -210
  491. oscura/exporters/vintage_logic_csv.py +0 -247
  492. oscura/reporting/vintage_logic_report.py +0 -523
  493. oscura/search/context.py +0 -149
  494. oscura/session/__init__.py +0 -34
  495. oscura/session/annotations.py +0 -289
  496. oscura/session/history.py +0 -313
  497. oscura/session/session.py +0 -520
  498. oscura/visualization/digital_advanced.py +0 -718
  499. oscura/visualization/figure_manager.py +0 -156
  500. oscura/workflow/__init__.py +0 -13
  501. oscura-0.5.0.dist-info/METADATA +0 -407
  502. oscura-0.5.0.dist-info/RECORD +0 -486
  503. /oscura/core/{config.py → config/legacy.py} +0 -0
  504. /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
  505. /oscura/{extensibility → core/extensibility}/registry.py +0 -0
  506. /oscura/{plugins → core/plugins}/isolation.py +0 -0
  507. /oscura/{builders → utils/builders}/signal_builder.py +0 -0
  508. /oscura/{optimization → utils/optimization}/parallel.py +0 -0
  509. /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
  510. /oscura/{streaming → utils/streaming}/__init__.py +0 -0
  511. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/WHEEL +0 -0
  512. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/entry_points.txt +0 -0
  513. {oscura-0.5.0.dist-info → oscura-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,668 @@
1
+ """CoAP protocol analyzer with RFC 7252 support and extensions.
2
+
3
+ Provides comprehensive CoAP message parsing, request/response matching,
4
+ blockwise transfer support (RFC 7959), and observe extension (RFC 7641).
5
+
6
+ Example:
7
+ >>> from oscura.iot.coap import CoAPAnalyzer
8
+ >>> analyzer = CoAPAnalyzer()
9
+ >>> data = bytes([0x40, 0x01, 0x12, 0x34]) # CON GET
10
+ >>> message = analyzer.parse_message(data, timestamp=0.0)
11
+ >>> print(message.msg_type, message.code)
12
+ CON GET
13
+
14
+ References:
15
+ RFC 7252: CoAP (Constrained Application Protocol)
16
+ RFC 7959: Block-Wise Transfers in CoAP
17
+ RFC 7641: Observing Resources in CoAP
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import json
23
+ from dataclasses import dataclass, field
24
+ from pathlib import Path
25
+ from typing import Any, ClassVar
26
+
27
+ from oscura.iot.coap.options import (
28
+ CONTENT_FORMATS,
29
+ OPTIONS,
30
+ OptionParser,
31
+ format_block_option,
32
+ )
33
+
34
+
35
+ @dataclass
36
+ class CoAPMessage:
37
+ """CoAP message representation.
38
+
39
+ Attributes:
40
+ timestamp: Message timestamp in seconds.
41
+ version: CoAP version (always 1 for RFC 7252).
42
+ msg_type: Message type ("CON", "NON", "ACK", "RST").
43
+ code: Method code or response code (e.g., "GET", "2.05 Content").
44
+ message_id: 16-bit message identifier for duplicate detection.
45
+ token: Token for matching requests/responses (0-8 bytes).
46
+ options: Parsed options dictionary (option name -> list of values).
47
+ payload: Message payload bytes.
48
+ is_request: True if request, False if response.
49
+ uri: Reconstructed URI from Uri-* options (optional).
50
+
51
+ Example:
52
+ >>> msg = CoAPMessage(
53
+ ... timestamp=1.0,
54
+ ... version=1,
55
+ ... msg_type="CON",
56
+ ... code="GET",
57
+ ... message_id=0x1234,
58
+ ... token=b"\\x01",
59
+ ... )
60
+ >>> msg.is_request
61
+ True
62
+ """
63
+
64
+ timestamp: float
65
+ version: int
66
+ msg_type: str
67
+ code: str
68
+ message_id: int
69
+ token: bytes
70
+ options: dict[str, list[Any]] = field(default_factory=dict)
71
+ payload: bytes = b""
72
+ is_request: bool = True
73
+ uri: str | None = None
74
+
75
+
76
+ @dataclass
77
+ class CoAPExchange:
78
+ """CoAP request-response exchange.
79
+
80
+ Represents a complete request-response transaction, including
81
+ support for multiple responses (observe pattern).
82
+
83
+ Attributes:
84
+ request: Initial CoAP request message.
85
+ responses: List of response messages (multiple for observe).
86
+ complete: True if exchange is complete (no more responses expected).
87
+ observe: True if this is an observe relationship.
88
+
89
+ Example:
90
+ >>> request = CoAPMessage(...)
91
+ >>> exchange = CoAPExchange(request=request)
92
+ >>> exchange.responses.append(response_msg)
93
+ """
94
+
95
+ request: CoAPMessage
96
+ responses: list[CoAPMessage] = field(default_factory=list)
97
+ complete: bool = False
98
+ observe: bool = False
99
+
100
+
101
+ class CoAPAnalyzer:
102
+ """CoAP protocol analyzer supporting RFC 7252 and extensions.
103
+
104
+ Analyzes CoAP messages including parsing message format, decoding options,
105
+ matching requests with responses, and handling blockwise transfers and
106
+ observe relationships.
107
+
108
+ Attributes:
109
+ MSG_TYPES: Message type code to name mapping.
110
+ METHODS: Method code to name mapping.
111
+ RESPONSE_CLASSES: Response class descriptions.
112
+
113
+ Example:
114
+ >>> analyzer = CoAPAnalyzer()
115
+ >>> msg = analyzer.parse_message(data, timestamp=0.0)
116
+ >>> analyzer.match_request_response()
117
+ >>> analyzer.export_exchanges(Path("coap_traffic.json"))
118
+ """
119
+
120
+ # Message types (RFC 7252 Section 3)
121
+ MSG_TYPES: ClassVar[dict[int, str]] = {
122
+ 0: "CON", # Confirmable
123
+ 1: "NON", # Non-confirmable
124
+ 2: "ACK", # Acknowledgement
125
+ 3: "RST", # Reset
126
+ }
127
+
128
+ # Method codes 0.xx (RFC 7252 Section 5.8)
129
+ METHODS: ClassVar[dict[int, str]] = {
130
+ 1: "GET",
131
+ 2: "POST",
132
+ 3: "PUT",
133
+ 4: "DELETE",
134
+ 5: "FETCH", # RFC 8132
135
+ 6: "PATCH", # RFC 8132
136
+ 7: "iPATCH", # RFC 8132
137
+ }
138
+
139
+ # Response code classes
140
+ RESPONSE_CLASSES: ClassVar[dict[int, str]] = {
141
+ 2: "Success",
142
+ 4: "Client Error",
143
+ 5: "Server Error",
144
+ }
145
+
146
+ def __init__(self) -> None:
147
+ """Initialize CoAP analyzer.
148
+
149
+ Example:
150
+ >>> analyzer = CoAPAnalyzer()
151
+ >>> len(analyzer.messages)
152
+ 0
153
+ """
154
+ self.messages: list[CoAPMessage] = []
155
+ self.exchanges: dict[bytes, CoAPExchange] = {} # Token -> exchange
156
+ self.message_id_map: dict[int, CoAPMessage] = {} # Message ID -> message
157
+ self.option_parser = OptionParser()
158
+
159
+ def parse_message(self, data: bytes, timestamp: float = 0.0) -> CoAPMessage:
160
+ """Parse CoAP message from bytes.
161
+
162
+ Parses complete CoAP message including header, token, options, and payload
163
+ according to RFC 7252 Section 3.
164
+
165
+ Message Format:
166
+ 0 1 2 3
167
+ 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
168
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
169
+ |Ver| T | TKL | Code | Message ID |
170
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
171
+ | Token (if any, TKL bytes) ...
172
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
173
+ | Options (if any) ...
174
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
175
+ |1 1 1 1 1 1 1 1| Payload (if any) ...
176
+ +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
177
+
178
+ Args:
179
+ data: Raw CoAP message bytes.
180
+ timestamp: Message timestamp in seconds.
181
+
182
+ Returns:
183
+ Parsed CoAP message.
184
+
185
+ Raises:
186
+ ValueError: If message is malformed or too short.
187
+
188
+ Example:
189
+ >>> analyzer = CoAPAnalyzer()
190
+ >>> data = bytes([0x40, 0x01, 0x12, 0x34]) # CON GET
191
+ >>> msg = analyzer.parse_message(data)
192
+ >>> msg.msg_type
193
+ 'CON'
194
+ >>> msg.code
195
+ 'GET'
196
+ """
197
+ if len(data) < 4:
198
+ raise ValueError(f"CoAP message too short: {len(data)} bytes (minimum 4)")
199
+
200
+ # Parse header and token
201
+ version, msg_type, code_str, is_request, message_id, token, offset = (
202
+ self._parse_header_and_token(data)
203
+ )
204
+
205
+ # Parse options
206
+ options = self._parse_options(data, offset)
207
+
208
+ # Find payload boundary
209
+ payload_start = self._find_payload_start(data, offset)
210
+
211
+ # Extract payload
212
+ payload = data[payload_start:] if payload_start < len(data) else b""
213
+
214
+ # Reconstruct URI from options
215
+ uri = self._reconstruct_uri(options)
216
+
217
+ message = CoAPMessage(
218
+ timestamp=timestamp,
219
+ version=version,
220
+ msg_type=msg_type,
221
+ code=code_str,
222
+ message_id=message_id,
223
+ token=token,
224
+ options=options,
225
+ payload=payload,
226
+ is_request=is_request,
227
+ uri=uri,
228
+ )
229
+
230
+ self.messages.append(message)
231
+ self.message_id_map[message_id] = message
232
+
233
+ return message
234
+
235
+ def _parse_header_and_token(self, data: bytes) -> tuple[int, str, str, bool, int, bytes, int]:
236
+ """Parse CoAP header and token fields.
237
+
238
+ Args:
239
+ data: Raw message bytes.
240
+
241
+ Returns:
242
+ Tuple of (version, msg_type, code_str, is_request, message_id, token, offset).
243
+
244
+ Raises:
245
+ ValueError: If header or token is invalid.
246
+ """
247
+ byte0 = data[0]
248
+ version = (byte0 >> 6) & 0x03
249
+ msg_type_val = (byte0 >> 4) & 0x03
250
+ tkl = byte0 & 0x0F
251
+
252
+ if version != 1:
253
+ raise ValueError(f"Unsupported CoAP version: {version} (expected 1)")
254
+
255
+ if tkl > 8:
256
+ raise ValueError(f"Invalid token length: {tkl} (maximum 8)")
257
+
258
+ code_byte = data[1]
259
+ message_id = int.from_bytes(data[2:4], "big")
260
+
261
+ msg_type = self.MSG_TYPES.get(msg_type_val, f"UNKNOWN({msg_type_val})")
262
+ code_str, is_request = self._parse_code(code_byte)
263
+
264
+ offset = 4
265
+
266
+ # Parse token (TKL bytes)
267
+ token = b""
268
+ if tkl > 0:
269
+ if len(data) < offset + tkl:
270
+ raise ValueError(f"Insufficient data for token: need {tkl} bytes")
271
+ token = data[offset : offset + tkl]
272
+ offset += tkl
273
+
274
+ return version, msg_type, code_str, is_request, message_id, token, offset
275
+
276
+ def _find_payload_start(self, data: bytes, start_offset: int) -> int:
277
+ """Find start of payload after options.
278
+
279
+ Args:
280
+ data: Complete message data.
281
+ start_offset: Offset where options start.
282
+
283
+ Returns:
284
+ Offset where payload starts.
285
+
286
+ Raises:
287
+ ValueError: If option encoding is invalid.
288
+ """
289
+ payload_start = start_offset
290
+ while payload_start < len(data):
291
+ if data[payload_start] == 0xFF:
292
+ payload_start += 1 # Skip marker
293
+ break
294
+ if payload_start >= len(data):
295
+ break
296
+
297
+ option_byte = data[payload_start]
298
+ delta = (option_byte >> 4) & 0x0F
299
+ length = option_byte & 0x0F
300
+
301
+ payload_start += 1
302
+
303
+ # Handle extended delta
304
+ if delta == 13:
305
+ payload_start += 1
306
+ elif delta == 14:
307
+ payload_start += 2
308
+ elif delta == 15:
309
+ break # Payload marker
310
+
311
+ # Handle extended length
312
+ if length == 13:
313
+ payload_start += 1
314
+ elif length == 14:
315
+ payload_start += 2
316
+ elif length == 15:
317
+ raise ValueError("Invalid option length encoding (15)")
318
+
319
+ # Skip option value
320
+ if delta < 15:
321
+ actual_length = self._calculate_option_length(data, payload_start, length)
322
+ payload_start += actual_length
323
+
324
+ return payload_start
325
+
326
+ def _calculate_option_length(self, data: bytes, offset: int, length_base: int) -> int:
327
+ """Calculate actual option length from extended encoding.
328
+
329
+ Args:
330
+ data: Message data.
331
+ offset: Current offset (after length encoding bytes).
332
+ length_base: Base length value from option byte.
333
+
334
+ Returns:
335
+ Actual option length in bytes.
336
+ """
337
+ if length_base < 13:
338
+ return length_base
339
+ elif length_base == 13 and offset <= len(data):
340
+ return data[offset - 1] + 13
341
+ elif length_base == 14 and offset + 1 <= len(data):
342
+ return int.from_bytes(data[offset - 2 : offset], "big") + 269
343
+ return 0
344
+
345
+ def _parse_code(self, code: int) -> tuple[str, bool]:
346
+ """Parse code byte into human-readable string and request flag.
347
+
348
+ Code byte format: class (3 bits) . detail (5 bits)
349
+ - 0.xx: Request methods
350
+ - 2.xx: Success responses
351
+ - 4.xx: Client error responses
352
+ - 5.xx: Server error responses
353
+
354
+ Args:
355
+ code: Code byte value (0-255).
356
+
357
+ Returns:
358
+ Tuple of (code_string, is_request).
359
+
360
+ Example:
361
+ >>> analyzer = CoAPAnalyzer()
362
+ >>> analyzer._parse_code(0x01)
363
+ ('GET', True)
364
+ >>> analyzer._parse_code(0x45)
365
+ ('2.05 Content', False)
366
+ """
367
+ code_class = (code >> 5) & 0x07
368
+ code_detail = code & 0x1F
369
+
370
+ if code_class == 0:
371
+ # Request method
372
+ method = self.METHODS.get(code_detail, f"0.{code_detail:02d}")
373
+ return method, True
374
+
375
+ # Response code
376
+ code_str = f"{code_class}.{code_detail:02d}"
377
+
378
+ # Add common response names
379
+ response_names = {
380
+ 0x41: "2.01 Created",
381
+ 0x42: "2.02 Deleted",
382
+ 0x43: "2.03 Valid",
383
+ 0x44: "2.04 Changed",
384
+ 0x45: "2.05 Content",
385
+ 0x5F: "2.31 Continue",
386
+ 0x80: "4.00 Bad Request",
387
+ 0x81: "4.01 Unauthorized",
388
+ 0x82: "4.02 Bad Option",
389
+ 0x83: "4.03 Forbidden",
390
+ 0x84: "4.04 Not Found",
391
+ 0x85: "4.05 Method Not Allowed",
392
+ 0x86: "4.06 Not Acceptable",
393
+ 0x8C: "4.12 Precondition Failed",
394
+ 0x8D: "4.13 Request Entity Too Large",
395
+ 0x8F: "4.15 Unsupported Content-Format",
396
+ 0xA0: "5.00 Internal Server Error",
397
+ 0xA1: "5.01 Not Implemented",
398
+ 0xA2: "5.02 Bad Gateway",
399
+ 0xA3: "5.03 Service Unavailable",
400
+ 0xA4: "5.04 Gateway Timeout",
401
+ 0xA5: "5.05 Proxying Not Supported",
402
+ }
403
+
404
+ return response_names.get(code, code_str), False
405
+
406
+ def _parse_options(self, data: bytes, start_offset: int) -> dict[str, list[Any]]:
407
+ """Parse CoAP options using delta encoding.
408
+
409
+ Options are encoded with delta encoding where each option number
410
+ is the sum of all previous deltas. Handles extended delta/length
411
+ encoding for values >= 13.
412
+
413
+ Args:
414
+ data: Complete message data.
415
+ start_offset: Offset where options start.
416
+
417
+ Returns:
418
+ Dictionary mapping option names to lists of decoded values.
419
+
420
+ Raises:
421
+ ValueError: If option encoding is invalid.
422
+
423
+ Example:
424
+ >>> analyzer = CoAPAnalyzer()
425
+ >>> # Parse message with Uri-Path option
426
+ >>> options = analyzer._parse_options(data, 5)
427
+ >>> options.get("Uri-Path", [])
428
+ ['temperature', 'sensor1']
429
+ """
430
+ options: dict[str, list[Any]] = {}
431
+ offset = start_offset
432
+ current_option_num = 0
433
+
434
+ while offset < len(data):
435
+ # Check for payload marker
436
+ if data[offset] == 0xFF:
437
+ break
438
+
439
+ if offset >= len(data):
440
+ break
441
+
442
+ option_byte = data[offset]
443
+ delta_base = (option_byte >> 4) & 0x0F
444
+ length_base = option_byte & 0x0F
445
+ offset += 1
446
+
447
+ # Check for payload marker in delta
448
+ if delta_base == 15:
449
+ offset -= 1 # Back up to marker
450
+ break
451
+
452
+ # Parse extended delta
453
+ try:
454
+ delta, delta_bytes = self.option_parser.parse_extended_value(
455
+ delta_base, data, offset
456
+ )
457
+ offset += delta_bytes
458
+ except ValueError as e:
459
+ raise ValueError(f"Failed to parse option delta: {e}") from e
460
+
461
+ # Parse extended length
462
+ try:
463
+ length, length_bytes = self.option_parser.parse_extended_value(
464
+ length_base, data, offset
465
+ )
466
+ offset += length_bytes
467
+ except ValueError as e:
468
+ raise ValueError(f"Failed to parse option length: {e}") from e
469
+
470
+ # Calculate actual option number
471
+ current_option_num += delta
472
+
473
+ # Extract option value
474
+ if len(data) < offset + length:
475
+ raise ValueError(
476
+ f"Insufficient data for option value: need {length} bytes at offset {offset}"
477
+ )
478
+
479
+ option_value = data[offset : offset + length]
480
+ offset += length
481
+
482
+ # Decode option value
483
+ decoded_value = self.option_parser.decode_value(current_option_num, option_value)
484
+
485
+ # Store option
486
+ option_name = OPTIONS.get(current_option_num, f"Option-{current_option_num}")
487
+
488
+ if option_name not in options:
489
+ options[option_name] = []
490
+
491
+ options[option_name].append(decoded_value)
492
+
493
+ return options
494
+
495
+ def _reconstruct_uri(self, options: dict[str, list[Any]]) -> str | None:
496
+ """Reconstruct URI from Uri-* options.
497
+
498
+ Combines Uri-Host, Uri-Port, Uri-Path, and Uri-Query options
499
+ into a complete URI string.
500
+
501
+ Args:
502
+ options: Parsed options dictionary.
503
+
504
+ Returns:
505
+ Reconstructed URI string, or None if no Uri options present.
506
+
507
+ Example:
508
+ >>> analyzer = CoAPAnalyzer()
509
+ >>> options = {
510
+ ... "Uri-Host": ["example.com"],
511
+ ... "Uri-Path": ["sensors", "temperature"],
512
+ ... "Uri-Query": ["format=json"],
513
+ ... }
514
+ >>> analyzer._reconstruct_uri(options)
515
+ 'coap://example.com/sensors/temperature?format=json'
516
+ """
517
+ if not any(key.startswith("Uri-") for key in options):
518
+ return None
519
+
520
+ # Build URI components
521
+ host = options.get("Uri-Host", [None])[0]
522
+ port = options.get("Uri-Port", [5683])[0] # Default CoAP port
523
+ path_segments = options.get("Uri-Path", [])
524
+ query_params = options.get("Uri-Query", [])
525
+
526
+ # Construct URI
527
+ uri_parts = []
528
+
529
+ if host:
530
+ scheme = "coap"
531
+ if isinstance(port, int) and port != 5683:
532
+ uri_parts.append(f"{scheme}://{host}:{port}")
533
+ else:
534
+ uri_parts.append(f"{scheme}://{host}")
535
+ else:
536
+ uri_parts.append("coap://")
537
+
538
+ # Add path
539
+ if path_segments:
540
+ path = "/" + "/".join(str(seg) for seg in path_segments)
541
+ uri_parts.append(path)
542
+
543
+ # Add query string
544
+ if query_params:
545
+ query = "&".join(str(param) for param in query_params)
546
+ uri_parts.append(f"?{query}")
547
+
548
+ return "".join(uri_parts)
549
+
550
+ def match_request_response(self) -> None:
551
+ """Match requests with responses by token and message ID.
552
+
553
+ Creates CoAPExchange objects linking requests with their responses.
554
+ Supports observe relationships where multiple responses map to one request.
555
+
556
+ Example:
557
+ >>> analyzer = CoAPAnalyzer()
558
+ >>> # Parse messages...
559
+ >>> analyzer.match_request_response()
560
+ >>> len(analyzer.exchanges)
561
+ 5
562
+ """
563
+ # Clear existing exchanges
564
+ self.exchanges = {}
565
+
566
+ for message in self.messages:
567
+ if message.is_request:
568
+ # Create new exchange for request
569
+ observe = "Observe" in message.options
570
+ exchange = CoAPExchange(request=message, observe=observe)
571
+ self.exchanges[message.token] = exchange
572
+ else:
573
+ # Match response to request by token
574
+ if message.token in self.exchanges:
575
+ exchange = self.exchanges[message.token]
576
+ exchange.responses.append(message)
577
+
578
+ # Mark complete if not observe or if ACK/RST
579
+ if not exchange.observe or message.msg_type in ("ACK", "RST"):
580
+ exchange.complete = True
581
+
582
+ def export_exchanges(self, output_path: Path) -> None:
583
+ """Export request-response exchanges as JSON.
584
+
585
+ Exports all matched exchanges including request, responses, timing,
586
+ and decoded options.
587
+
588
+ Args:
589
+ output_path: Path to output JSON file.
590
+
591
+ Example:
592
+ >>> analyzer = CoAPAnalyzer()
593
+ >>> # Parse and match messages...
594
+ >>> analyzer.export_exchanges(Path("coap_exchanges.json"))
595
+ """
596
+ export_data: dict[str, Any] = {
597
+ "summary": {
598
+ "total_messages": len(self.messages),
599
+ "total_exchanges": len(self.exchanges),
600
+ "complete_exchanges": sum(1 for ex in self.exchanges.values() if ex.complete),
601
+ },
602
+ "exchanges": [],
603
+ }
604
+
605
+ for token, exchange in self.exchanges.items():
606
+ request_data = self._message_to_dict(exchange.request)
607
+
608
+ responses_data = [self._message_to_dict(resp) for resp in exchange.responses]
609
+
610
+ exchange_entry = {
611
+ "token": token.hex() if token else "",
612
+ "observe": exchange.observe,
613
+ "complete": exchange.complete,
614
+ "request": request_data,
615
+ "responses": responses_data,
616
+ "response_count": len(exchange.responses),
617
+ }
618
+
619
+ export_data["exchanges"].append(exchange_entry)
620
+
621
+ # Write JSON
622
+ with output_path.open("w") as f:
623
+ json.dump(export_data, f, indent=2)
624
+
625
+ def _message_to_dict(self, message: CoAPMessage) -> dict[str, Any]:
626
+ """Convert CoAPMessage to dictionary for JSON export.
627
+
628
+ Args:
629
+ message: CoAP message to convert.
630
+
631
+ Returns:
632
+ Dictionary representation of message.
633
+ """
634
+ # Format options for readability
635
+ formatted_options: dict[str, list[Any]] = {}
636
+ for name, values in message.options.items():
637
+ formatted_values: list[Any] = []
638
+ for value in values:
639
+ if isinstance(value, bytes):
640
+ formatted_values.append(value.hex())
641
+ elif isinstance(value, int) and name == "Content-Format":
642
+ # Add content format name
643
+ format_name = CONTENT_FORMATS.get(value, "unknown")
644
+ formatted_values.append({"code": value, "format": format_name})
645
+ elif isinstance(value, int) and name in ("Block1", "Block2"):
646
+ # Decode block option
647
+ block_info = format_block_option(value)
648
+ formatted_values.append(block_info)
649
+ else:
650
+ formatted_values.append(value)
651
+ formatted_options[name] = formatted_values
652
+
653
+ return {
654
+ "timestamp": message.timestamp,
655
+ "type": message.msg_type,
656
+ "code": message.code,
657
+ "message_id": f"0x{message.message_id:04X}",
658
+ "token": message.token.hex() if message.token else "",
659
+ "uri": message.uri,
660
+ "options": formatted_options,
661
+ "payload_length": len(message.payload),
662
+ "payload": message.payload.hex()
663
+ if len(message.payload) <= 64
664
+ else f"{message.payload[:64].hex()}... ({len(message.payload)} bytes)",
665
+ }
666
+
667
+
668
+ __all__ = ["CoAPAnalyzer", "CoAPExchange", "CoAPMessage"]