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,999 @@
1
+ """MQTT protocol analyzer for versions 3.1.1 and 5.0.
2
+
3
+ This module provides comprehensive MQTT protocol analysis including packet
4
+ parsing, session tracking, and topic hierarchy discovery.
5
+
6
+ Example:
7
+ >>> from oscura.iot.mqtt import MQTTAnalyzer
8
+ >>> analyzer = MQTTAnalyzer()
9
+ >>> packet = analyzer.parse_packet(data, timestamp=0.0)
10
+ >>> topology = analyzer.get_topic_hierarchy()
11
+
12
+ References:
13
+ MQTT 3.1.1: http://docs.oasis-open.org/mqtt/mqtt/v3.1.1/
14
+ MQTT 5.0: https://docs.oasis-open.org/mqtt/mqtt/v5.0/
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ import json
20
+ from dataclasses import dataclass, field
21
+ from pathlib import Path
22
+ from typing import Any, ClassVar
23
+
24
+ from oscura.iot.mqtt.properties import parse_properties
25
+
26
+
27
+ @dataclass
28
+ class MQTTPacket:
29
+ """MQTT control packet representation.
30
+
31
+ Attributes:
32
+ timestamp: Packet timestamp in seconds.
33
+ packet_type: Packet type name ("CONNECT", "PUBLISH", etc.).
34
+ protocol_version: MQTT protocol version ("3.1.1" or "5.0").
35
+ flags: Packet flags (DUP, QoS, RETAIN, etc.).
36
+ packet_id: Packet identifier for QoS > 0 (optional).
37
+ topic: Topic name for PUBLISH/SUBSCRIBE packets (optional).
38
+ payload: Packet payload bytes.
39
+ properties: MQTT 5.0 properties dictionary.
40
+ qos: Quality of Service level (0, 1, or 2).
41
+
42
+ Example:
43
+ >>> packet = MQTTPacket(
44
+ ... timestamp=0.0,
45
+ ... packet_type="PUBLISH",
46
+ ... protocol_version="3.1.1",
47
+ ... flags={"dup": False, "qos": 0, "retain": False},
48
+ ... topic="home/sensor/temperature",
49
+ ... payload=b"22.5",
50
+ ... qos=0,
51
+ ... )
52
+ """
53
+
54
+ timestamp: float
55
+ packet_type: str
56
+ protocol_version: str
57
+ flags: dict[str, bool | int]
58
+ packet_id: int | None = None
59
+ topic: str | None = None
60
+ payload: bytes = b""
61
+ properties: dict[str, Any] = field(default_factory=dict)
62
+ qos: int = 0
63
+
64
+
65
+ @dataclass
66
+ class MQTTSession:
67
+ """MQTT session information.
68
+
69
+ Tracks connection details and topic subscriptions for a client.
70
+
71
+ Attributes:
72
+ client_id: Client identifier string.
73
+ username: Authentication username (optional).
74
+ protocol_version: MQTT protocol version ("3.1.1" or "5.0").
75
+ keep_alive: Keep-alive interval in seconds.
76
+ clean_session: Clean session flag (3.1.1) or clean start flag (5.0).
77
+ will_topic: Last Will and Testament topic (optional).
78
+ will_message: Last Will and Testament message payload (optional).
79
+ subscribed_topics: List of subscribed topic filters.
80
+ published_topics: Dictionary mapping topics to publish counts.
81
+
82
+ Example:
83
+ >>> session = MQTTSession(
84
+ ... client_id="sensor_01",
85
+ ... username="admin",
86
+ ... protocol_version="3.1.1",
87
+ ... keep_alive=60,
88
+ ... )
89
+ """
90
+
91
+ client_id: str
92
+ username: str | None = None
93
+ protocol_version: str = "3.1.1"
94
+ keep_alive: int = 60
95
+ clean_session: bool = True
96
+ will_topic: str | None = None
97
+ will_message: bytes | None = None
98
+ subscribed_topics: list[str] = field(default_factory=list)
99
+ published_topics: dict[str, int] = field(default_factory=dict)
100
+
101
+
102
+ class MQTTAnalyzer:
103
+ """MQTT protocol analyzer for versions 3.1.1 and 5.0.
104
+
105
+ Parses MQTT control packets, tracks sessions, and builds topic hierarchies.
106
+
107
+ Attributes:
108
+ PACKET_TYPES: Mapping of packet type codes to names.
109
+
110
+ Example:
111
+ >>> analyzer = MQTTAnalyzer()
112
+ >>> packet = analyzer.parse_packet(mqtt_data)
113
+ >>> hierarchy = analyzer.get_topic_hierarchy()
114
+ >>> analyzer.export_topology(Path("mqtt_topology.json"))
115
+ """
116
+
117
+ # MQTT control packet types
118
+ PACKET_TYPES: ClassVar[dict[int, str]] = {
119
+ 1: "CONNECT",
120
+ 2: "CONNACK",
121
+ 3: "PUBLISH",
122
+ 4: "PUBACK",
123
+ 5: "PUBREC",
124
+ 6: "PUBREL",
125
+ 7: "PUBCOMP",
126
+ 8: "SUBSCRIBE",
127
+ 9: "SUBACK",
128
+ 10: "UNSUBSCRIBE",
129
+ 11: "UNSUBACK",
130
+ 12: "PINGREQ",
131
+ 13: "PINGRESP",
132
+ 14: "DISCONNECT",
133
+ 15: "AUTH",
134
+ }
135
+
136
+ def __init__(self) -> None:
137
+ """Initialize MQTT analyzer.
138
+
139
+ Example:
140
+ >>> analyzer = MQTTAnalyzer()
141
+ >>> len(analyzer.packets)
142
+ 0
143
+ """
144
+ self.packets: list[MQTTPacket] = []
145
+ self.sessions: dict[str, MQTTSession] = {}
146
+ self.topics: set[str] = set()
147
+
148
+ def _calculate_header_size(self, data: bytes) -> int:
149
+ """Calculate fixed header size including length encoding.
150
+
151
+ Args:
152
+ data: Raw packet data
153
+
154
+ Returns:
155
+ Size of fixed header in bytes
156
+ """
157
+ header_size = 1
158
+ temp_data = data[1:]
159
+ while temp_data and (temp_data[0] & 0x80):
160
+ header_size += 1
161
+ temp_data = temp_data[1:]
162
+ return header_size + 1
163
+
164
+ def _parse_packet_data(
165
+ self, packet_type: str, var_header_payload: bytes, flags: int
166
+ ) -> dict[str, Any]:
167
+ """Parse variable header and payload based on packet type.
168
+
169
+ Args:
170
+ packet_type: Type of MQTT packet
171
+ var_header_payload: Variable header and payload bytes
172
+ flags: Packet flags
173
+
174
+ Returns:
175
+ Parsed packet data dictionary
176
+ """
177
+ parsers: dict[str, Any] = {
178
+ "CONNECT": lambda: self._parse_connect(var_header_payload),
179
+ "PUBLISH": lambda: self._parse_publish(var_header_payload, flags),
180
+ "SUBSCRIBE": lambda: self._parse_subscribe(var_header_payload),
181
+ "SUBACK": lambda: self._parse_suback(var_header_payload),
182
+ "UNSUBSCRIBE": lambda: self._parse_unsubscribe(var_header_payload),
183
+ "CONNACK": lambda: self._parse_connack(var_header_payload),
184
+ "DISCONNECT": lambda: self._parse_disconnect(var_header_payload),
185
+ "AUTH": lambda: self._parse_auth(var_header_payload),
186
+ }
187
+
188
+ if packet_type in parsers:
189
+ # Lambda returns dict[str, Any] from parser methods
190
+ from typing import cast
191
+
192
+ return cast("dict[str, Any]", parsers[packet_type]())
193
+
194
+ if packet_type in ["PUBACK", "PUBREC", "PUBREL", "PUBCOMP", "UNSUBACK"]:
195
+ return self._parse_ack(var_header_payload, packet_type)
196
+
197
+ if packet_type in ["PINGREQ", "PINGRESP"]:
198
+ return {}
199
+
200
+ return {"payload": var_header_payload}
201
+
202
+ def _track_packet_metadata(self, packet: MQTTPacket, parsed_data: dict[str, Any]) -> None:
203
+ """Track packet metadata (topics, sessions, subscriptions).
204
+
205
+ Args:
206
+ packet: Parsed MQTT packet
207
+ parsed_data: Raw parsed data
208
+ """
209
+ if packet.topic:
210
+ self.topics.add(packet.topic)
211
+
212
+ if packet.packet_type == "CONNECT" and "client_id" in parsed_data:
213
+ self._track_session(parsed_data)
214
+
215
+ if packet.packet_type == "SUBSCRIBE" and "topics" in parsed_data:
216
+ for topic_filter, _ in parsed_data["topics"]:
217
+ self.topics.add(topic_filter)
218
+
219
+ def parse_packet(self, data: bytes, timestamp: float = 0.0) -> MQTTPacket:
220
+ """Parse MQTT control packet.
221
+
222
+ Args:
223
+ data: Raw MQTT packet bytes.
224
+ timestamp: Packet timestamp in seconds.
225
+
226
+ Returns:
227
+ Parsed MQTT packet.
228
+
229
+ Raises:
230
+ ValueError: If packet is malformed or incomplete.
231
+
232
+ Example:
233
+ >>> analyzer = MQTTAnalyzer()
234
+ >>> # CONNECT packet
235
+ >>> data = b"\\x10\\x10\\x00\\x04MQTT\\x04\\x02\\x00\\x3c\\x00\\x04test"
236
+ >>> packet = analyzer.parse_packet(data)
237
+ >>> packet.packet_type
238
+ 'CONNECT'
239
+ """
240
+ if len(data) < 2:
241
+ raise ValueError("Insufficient data for MQTT packet")
242
+
243
+ # Parse fixed header and extract variable header/payload
244
+ packet_type, var_header_payload, flags = self._extract_packet_components(data)
245
+
246
+ # Parse variable header and payload
247
+ parsed_data = self._parse_packet_data(packet_type, var_header_payload, flags)
248
+
249
+ # Create packet object
250
+ packet = self._create_packet(timestamp, packet_type, parsed_data)
251
+
252
+ # Track metadata
253
+ self.packets.append(packet)
254
+ self._track_packet_metadata(packet, parsed_data)
255
+
256
+ return packet
257
+
258
+ def _extract_packet_components(self, data: bytes) -> tuple[str, bytes, int]:
259
+ """Extract packet type and variable header/payload from data.
260
+
261
+ Args:
262
+ data: Raw MQTT packet bytes.
263
+
264
+ Returns:
265
+ Tuple of (packet_type, var_header_payload, flags).
266
+
267
+ Raises:
268
+ ValueError: If packet is malformed or incomplete.
269
+ """
270
+ packet_type_code, flags, remaining_length = self._parse_fixed_header(data)
271
+
272
+ if packet_type_code not in self.PACKET_TYPES:
273
+ raise ValueError(f"Unknown packet type: {packet_type_code}")
274
+
275
+ packet_type = self.PACKET_TYPES[packet_type_code]
276
+ header_size = self._calculate_header_size(data)
277
+
278
+ if len(data) < header_size + remaining_length:
279
+ raise ValueError("Incomplete packet data")
280
+
281
+ var_header_payload = data[header_size : header_size + remaining_length]
282
+ return packet_type, var_header_payload, flags
283
+
284
+ def _create_packet(
285
+ self, timestamp: float, packet_type: str, parsed_data: dict[str, Any]
286
+ ) -> MQTTPacket:
287
+ """Create MQTTPacket from parsed data.
288
+
289
+ Args:
290
+ timestamp: Packet timestamp.
291
+ packet_type: Packet type string.
292
+ parsed_data: Parsed packet data dictionary.
293
+
294
+ Returns:
295
+ MQTTPacket instance.
296
+ """
297
+ return MQTTPacket(
298
+ timestamp=timestamp,
299
+ packet_type=packet_type,
300
+ protocol_version=parsed_data.get("protocol_version", "3.1.1"),
301
+ flags=parsed_data.get("flags", {}),
302
+ packet_id=parsed_data.get("packet_id"),
303
+ topic=parsed_data.get("topic"),
304
+ payload=parsed_data.get("payload", b""),
305
+ properties=parsed_data.get("properties", {}),
306
+ qos=parsed_data.get("qos", 0),
307
+ )
308
+
309
+ def _parse_fixed_header(self, data: bytes) -> tuple[int, int, int]:
310
+ """Parse MQTT fixed header.
311
+
312
+ The fixed header consists of:
313
+ - Byte 1: Control Packet type (bits 4-7) and Flags (bits 0-3)
314
+ - Byte 2+: Remaining Length (variable length encoding)
315
+
316
+ Args:
317
+ data: Raw packet data.
318
+
319
+ Returns:
320
+ Tuple of (packet_type, flags, remaining_length).
321
+
322
+ Raises:
323
+ ValueError: If header is malformed.
324
+
325
+ Example:
326
+ >>> analyzer = MQTTAnalyzer()
327
+ >>> # PUBLISH packet: type=3, flags=0, length=10
328
+ >>> data = b"\\x30\\x0a..."
329
+ >>> ptype, flags, length = analyzer._parse_fixed_header(data)
330
+ >>> ptype
331
+ 3
332
+ """
333
+ if len(data) < 2:
334
+ raise ValueError("Insufficient data for fixed header")
335
+
336
+ byte1 = data[0]
337
+ packet_type = (byte1 >> 4) & 0x0F
338
+ flags = byte1 & 0x0F
339
+
340
+ # Decode remaining length (variable byte integer)
341
+ multiplier = 1
342
+ value = 0
343
+ index = 1
344
+
345
+ while True:
346
+ if index >= len(data):
347
+ raise ValueError("Incomplete remaining length")
348
+
349
+ encoded_byte = data[index]
350
+ value += (encoded_byte & 0x7F) * multiplier
351
+
352
+ if (encoded_byte & 0x80) == 0:
353
+ break
354
+
355
+ multiplier *= 128
356
+ index += 1
357
+
358
+ if multiplier > 128 * 128 * 128:
359
+ raise ValueError("Remaining length exceeds maximum")
360
+
361
+ return packet_type, flags, value
362
+
363
+ def _parse_mqtt_string(self, data: bytes, offset: int) -> tuple[str, int]:
364
+ """Parse MQTT UTF-8 encoded string.
365
+
366
+ Args:
367
+ data: Packet data buffer.
368
+ offset: Current offset in buffer.
369
+
370
+ Returns:
371
+ Tuple of (parsed_string, new_offset).
372
+
373
+ Raises:
374
+ ValueError: If string data is incomplete.
375
+ """
376
+ if len(data) < offset + 2:
377
+ raise ValueError("Incomplete string length field")
378
+
379
+ str_len = int.from_bytes(data[offset : offset + 2], "big")
380
+ offset += 2
381
+
382
+ if len(data) < offset + str_len:
383
+ raise ValueError("Incomplete string data")
384
+
385
+ parsed_str = data[offset : offset + str_len].decode("utf-8")
386
+ return parsed_str, offset + str_len
387
+
388
+ def _parse_mqtt_binary(self, data: bytes, offset: int) -> tuple[bytes, int]:
389
+ """Parse MQTT binary data field.
390
+
391
+ Args:
392
+ data: Packet data buffer.
393
+ offset: Current offset in buffer.
394
+
395
+ Returns:
396
+ Tuple of (binary_data, new_offset).
397
+
398
+ Raises:
399
+ ValueError: If binary data is incomplete.
400
+ """
401
+ if len(data) < offset + 2:
402
+ raise ValueError("Incomplete binary length field")
403
+
404
+ bin_len = int.from_bytes(data[offset : offset + 2], "big")
405
+ offset += 2
406
+
407
+ if len(data) < offset + bin_len:
408
+ raise ValueError("Incomplete binary data")
409
+
410
+ binary_data = data[offset : offset + bin_len]
411
+ return binary_data, offset + bin_len
412
+
413
+ def _parse_connect_flags(self, connect_flags: int) -> dict[str, bool | int]:
414
+ """Parse CONNECT packet flags byte.
415
+
416
+ Args:
417
+ connect_flags: Flags byte from CONNECT packet.
418
+
419
+ Returns:
420
+ Dictionary of parsed flag values.
421
+ """
422
+ return {
423
+ "clean_session": bool(connect_flags & 0x02),
424
+ "will_flag": bool(connect_flags & 0x04),
425
+ "will_qos": (connect_flags >> 3) & 0x03,
426
+ "will_retain": bool(connect_flags & 0x20),
427
+ "username_flag": bool(connect_flags & 0x40),
428
+ "password_flag": bool(connect_flags & 0x80),
429
+ }
430
+
431
+ def _parse_will_data(
432
+ self, data: bytes, offset: int, protocol_version: str
433
+ ) -> tuple[str, bytes, int]:
434
+ """Parse Will topic and message from CONNECT packet.
435
+
436
+ Args:
437
+ data: Packet data buffer.
438
+ offset: Current offset in buffer.
439
+ protocol_version: MQTT protocol version.
440
+
441
+ Returns:
442
+ Tuple of (will_topic, will_message, new_offset).
443
+
444
+ Raises:
445
+ ValueError: If will data is incomplete.
446
+ """
447
+ # Will properties (MQTT 5.0 only)
448
+ if protocol_version == "5.0":
449
+ _, consumed = parse_properties(data, offset)
450
+ offset += consumed
451
+
452
+ # Will topic
453
+ will_topic, offset = self._parse_mqtt_string(data, offset)
454
+
455
+ # Will message
456
+ will_message, offset = self._parse_mqtt_binary(data, offset)
457
+
458
+ return will_topic, will_message, offset
459
+
460
+ def _parse_connect(self, data: bytes) -> dict[str, Any]:
461
+ """Parse CONNECT packet.
462
+
463
+ Args:
464
+ data: Variable header and payload bytes.
465
+
466
+ Returns:
467
+ Parsed CONNECT fields.
468
+
469
+ Raises:
470
+ ValueError: If packet is malformed.
471
+
472
+ Example:
473
+ >>> analyzer = MQTTAnalyzer()
474
+ >>> # MQTT 3.1.1 CONNECT
475
+ >>> data = b"\\x00\\x04MQTT\\x04\\x02\\x00\\x3c\\x00\\x04test"
476
+ >>> result = analyzer._parse_connect(data)
477
+ >>> result["protocol_version"]
478
+ '3.1.1'
479
+ """
480
+ if len(data) < 10:
481
+ raise ValueError("Insufficient data for CONNECT packet")
482
+
483
+ offset = 0
484
+
485
+ # Protocol name
486
+ protocol_name, offset = self._parse_mqtt_string(data, offset)
487
+
488
+ # Protocol level
489
+ protocol_level = data[offset]
490
+ offset += 1
491
+
492
+ # Map protocol level to version
493
+ version_map = {3: "3.1", 4: "3.1.1", 5: "5.0"}
494
+ protocol_version = version_map.get(protocol_level, "3.1.1")
495
+
496
+ # Connect flags
497
+ connect_flags = data[offset]
498
+ offset += 1
499
+ flags = self._parse_connect_flags(connect_flags)
500
+
501
+ # Keep alive
502
+ keep_alive = int.from_bytes(data[offset : offset + 2], "big")
503
+ offset += 2
504
+
505
+ # MQTT 5.0 properties
506
+ properties: dict[str, Any] = {}
507
+ if protocol_version == "5.0":
508
+ properties, consumed = parse_properties(data, offset)
509
+ offset += consumed
510
+
511
+ # Client ID
512
+ client_id, offset = self._parse_mqtt_string(data, offset)
513
+
514
+ # Will topic and message
515
+ will_topic = None
516
+ will_message = None
517
+ if flags["will_flag"]:
518
+ will_topic, will_message, offset = self._parse_will_data(data, offset, protocol_version)
519
+
520
+ # Username
521
+ username = None
522
+ if flags["username_flag"]:
523
+ username, offset = self._parse_mqtt_string(data, offset)
524
+
525
+ # Password
526
+ password = None
527
+ if flags["password_flag"]:
528
+ password, offset = self._parse_mqtt_binary(data, offset)
529
+
530
+ return {
531
+ "protocol_name": protocol_name,
532
+ "protocol_version": protocol_version,
533
+ "flags": flags,
534
+ "keep_alive": keep_alive,
535
+ "properties": properties,
536
+ "client_id": client_id,
537
+ "will_topic": will_topic,
538
+ "will_message": will_message,
539
+ "username": username,
540
+ "password": password,
541
+ }
542
+
543
+ def _parse_publish(self, data: bytes, flags: int) -> dict[str, Any]:
544
+ """Parse PUBLISH packet.
545
+
546
+ Args:
547
+ data: Variable header and payload bytes.
548
+ flags: Fixed header flags byte.
549
+
550
+ Returns:
551
+ Parsed PUBLISH fields.
552
+
553
+ Raises:
554
+ ValueError: If packet is malformed.
555
+
556
+ Example:
557
+ >>> analyzer = MQTTAnalyzer()
558
+ >>> # PUBLISH to "test" with QoS 0
559
+ >>> data = b"\\x00\\x04test22.5"
560
+ >>> result = analyzer._parse_publish(data, 0x00)
561
+ >>> result["topic"]
562
+ 'test'
563
+ """
564
+ qos = (flags >> 1) & 0x03
565
+ dup = bool(flags & 0x08)
566
+ retain = bool(flags & 0x01)
567
+
568
+ if len(data) < 2:
569
+ raise ValueError("Insufficient data for topic name length")
570
+
571
+ # Parse topic name
572
+ topic_len = int.from_bytes(data[0:2], "big")
573
+ if len(data) < 2 + topic_len:
574
+ raise ValueError("Insufficient data for topic name")
575
+
576
+ topic = data[2 : 2 + topic_len].decode("utf-8")
577
+ offset = 2 + topic_len
578
+
579
+ # Parse packet identifier (if QoS > 0)
580
+ packet_id = None
581
+ if qos > 0:
582
+ if len(data) < offset + 2:
583
+ raise ValueError("Insufficient data for packet ID")
584
+ packet_id = int.from_bytes(data[offset : offset + 2], "big")
585
+ offset += 2
586
+
587
+ # Parse properties (MQTT 5.0) - detect by checking if next byte looks like property length
588
+ properties: dict[str, Any] = {}
589
+ # We'll skip property parsing here for simplicity in 3.1.1 mode
590
+ # In 5.0, properties would be parsed before payload
591
+
592
+ # Remaining data is payload
593
+ payload = data[offset:]
594
+
595
+ return {
596
+ "topic": topic,
597
+ "qos": qos,
598
+ "flags": {"dup": dup, "qos": qos, "retain": retain},
599
+ "packet_id": packet_id,
600
+ "payload": payload,
601
+ "properties": properties,
602
+ }
603
+
604
+ def _parse_subscribe(self, data: bytes) -> dict[str, Any]:
605
+ """Parse SUBSCRIBE packet.
606
+
607
+ Args:
608
+ data: Variable header and payload bytes.
609
+
610
+ Returns:
611
+ Parsed SUBSCRIBE fields with topic filters.
612
+
613
+ Raises:
614
+ ValueError: If packet is malformed.
615
+
616
+ Example:
617
+ >>> analyzer = MQTTAnalyzer()
618
+ >>> # SUBSCRIBE to "test/+" with QoS 1
619
+ >>> data = b"\\x00\\x01\\x00\\x06test/+\\x01"
620
+ >>> result = analyzer._parse_subscribe(data)
621
+ >>> result["topics"][0]
622
+ ('test/+', 1)
623
+ """
624
+ if len(data) < 2:
625
+ raise ValueError("Insufficient data for packet ID")
626
+
627
+ # Packet identifier
628
+ packet_id = int.from_bytes(data[0:2], "big")
629
+ offset = 2
630
+
631
+ # Properties (MQTT 5.0) - skip for simplicity
632
+ properties: dict[str, Any] = {}
633
+
634
+ # Topic filters
635
+ topics = []
636
+ while offset < len(data):
637
+ if len(data) < offset + 2:
638
+ raise ValueError("Incomplete topic filter")
639
+
640
+ topic_len = int.from_bytes(data[offset : offset + 2], "big")
641
+ offset += 2
642
+
643
+ if len(data) < offset + topic_len:
644
+ raise ValueError("Incomplete topic filter")
645
+
646
+ topic_filter = data[offset : offset + topic_len].decode("utf-8")
647
+ offset += topic_len
648
+
649
+ if offset >= len(data):
650
+ raise ValueError("Missing subscription options")
651
+
652
+ # Subscription options (QoS in lower 2 bits for 3.1.1)
653
+ options = data[offset]
654
+ qos = options & 0x03
655
+ offset += 1
656
+
657
+ topics.append((topic_filter, qos))
658
+
659
+ return {
660
+ "packet_id": packet_id,
661
+ "properties": properties,
662
+ "topics": topics,
663
+ }
664
+
665
+ def _parse_ack(self, data: bytes, packet_type: str) -> dict[str, Any]:
666
+ """Parse acknowledgment packets (PUBACK, PUBREC, PUBREL, PUBCOMP, UNSUBACK).
667
+
668
+ Args:
669
+ data: Variable header bytes.
670
+ packet_type: Type of acknowledgment packet.
671
+
672
+ Returns:
673
+ Parsed acknowledgment fields.
674
+
675
+ Raises:
676
+ ValueError: If packet is malformed.
677
+
678
+ Example:
679
+ >>> analyzer = MQTTAnalyzer()
680
+ >>> data = b"\\x00\\x01" # Packet ID 1
681
+ >>> result = analyzer._parse_ack(data, "PUBACK")
682
+ >>> result["packet_id"]
683
+ 1
684
+ """
685
+ if len(data) < 2:
686
+ raise ValueError(f"Insufficient data for {packet_type} packet ID")
687
+
688
+ packet_id = int.from_bytes(data[0:2], "big")
689
+
690
+ # MQTT 5.0 may have reason code and properties
691
+ reason_code = None
692
+ properties: dict[str, Any] = {}
693
+
694
+ if len(data) > 2:
695
+ reason_code = data[2]
696
+ if len(data) > 3:
697
+ properties, _ = parse_properties(data, 3)
698
+
699
+ return {
700
+ "packet_id": packet_id,
701
+ "reason_code": reason_code,
702
+ "properties": properties,
703
+ }
704
+
705
+ def _parse_suback(self, data: bytes) -> dict[str, Any]:
706
+ """Parse SUBACK packet.
707
+
708
+ Args:
709
+ data: Variable header and payload bytes.
710
+
711
+ Returns:
712
+ Parsed SUBACK fields with return codes.
713
+
714
+ Raises:
715
+ ValueError: If packet is malformed.
716
+
717
+ Example:
718
+ >>> analyzer = MQTTAnalyzer()
719
+ >>> data = b"\\x00\\x01\\x00\\x01" # Packet ID 1, QoS 0 and 1 granted
720
+ >>> result = analyzer._parse_suback(data)
721
+ >>> result["return_codes"]
722
+ [0, 1]
723
+ """
724
+ if len(data) < 2:
725
+ raise ValueError("Insufficient data for SUBACK packet ID")
726
+
727
+ packet_id = int.from_bytes(data[0:2], "big")
728
+ offset = 2
729
+
730
+ # Properties (MQTT 5.0)
731
+ properties: dict[str, Any] = {}
732
+
733
+ # Return codes
734
+ return_codes = list(data[offset:])
735
+
736
+ return {
737
+ "packet_id": packet_id,
738
+ "properties": properties,
739
+ "return_codes": return_codes,
740
+ }
741
+
742
+ def _parse_unsubscribe(self, data: bytes) -> dict[str, Any]:
743
+ """Parse UNSUBSCRIBE packet.
744
+
745
+ Args:
746
+ data: Variable header and payload bytes.
747
+
748
+ Returns:
749
+ Parsed UNSUBSCRIBE fields with topic filters.
750
+
751
+ Raises:
752
+ ValueError: If packet is malformed.
753
+
754
+ Example:
755
+ >>> analyzer = MQTTAnalyzer()
756
+ >>> data = b"\\x00\\x01\\x00\\x04test"
757
+ >>> result = analyzer._parse_unsubscribe(data)
758
+ >>> result["topics"]
759
+ ['test']
760
+ """
761
+ if len(data) < 2:
762
+ raise ValueError("Insufficient data for packet ID")
763
+
764
+ packet_id = int.from_bytes(data[0:2], "big")
765
+ offset = 2
766
+
767
+ # Properties (MQTT 5.0)
768
+ properties: dict[str, Any] = {}
769
+
770
+ # Topic filters
771
+ topics = []
772
+ while offset < len(data):
773
+ if len(data) < offset + 2:
774
+ raise ValueError("Incomplete topic filter")
775
+
776
+ topic_len = int.from_bytes(data[offset : offset + 2], "big")
777
+ offset += 2
778
+
779
+ if len(data) < offset + topic_len:
780
+ raise ValueError("Incomplete topic filter")
781
+
782
+ topic_filter = data[offset : offset + topic_len].decode("utf-8")
783
+ offset += topic_len
784
+
785
+ topics.append(topic_filter)
786
+
787
+ return {
788
+ "packet_id": packet_id,
789
+ "properties": properties,
790
+ "topics": topics,
791
+ }
792
+
793
+ def _parse_connack(self, data: bytes) -> dict[str, Any]:
794
+ """Parse CONNACK packet.
795
+
796
+ Args:
797
+ data: Variable header bytes.
798
+
799
+ Returns:
800
+ Parsed CONNACK fields.
801
+
802
+ Raises:
803
+ ValueError: If packet is malformed.
804
+
805
+ Example:
806
+ >>> analyzer = MQTTAnalyzer()
807
+ >>> data = b"\\x00\\x00" # Session present=0, return code=0 (success)
808
+ >>> result = analyzer._parse_connack(data)
809
+ >>> result["return_code"]
810
+ 0
811
+ """
812
+ if len(data) < 2:
813
+ raise ValueError("Insufficient data for CONNACK")
814
+
815
+ acknowledge_flags = data[0]
816
+ session_present = bool(acknowledge_flags & 0x01)
817
+
818
+ return_code = data[1]
819
+
820
+ # Properties (MQTT 5.0)
821
+ properties: dict[str, Any] = {}
822
+ if len(data) > 2:
823
+ properties, _ = parse_properties(data, 2)
824
+
825
+ return {
826
+ "flags": {"session_present": session_present},
827
+ "return_code": return_code,
828
+ "properties": properties,
829
+ }
830
+
831
+ def _parse_disconnect(self, data: bytes) -> dict[str, Any]:
832
+ """Parse DISCONNECT packet.
833
+
834
+ Args:
835
+ data: Variable header bytes.
836
+
837
+ Returns:
838
+ Parsed DISCONNECT fields.
839
+
840
+ Example:
841
+ >>> analyzer = MQTTAnalyzer()
842
+ >>> data = b"" # Empty for MQTT 3.1.1
843
+ >>> result = analyzer._parse_disconnect(data)
844
+ >>> result
845
+ {}
846
+ """
847
+ # MQTT 3.1.1 has no variable header
848
+ if len(data) == 0:
849
+ return {}
850
+
851
+ # MQTT 5.0 has reason code and properties
852
+ reason_code = data[0] if len(data) > 0 else 0
853
+ properties: dict[str, Any] = {}
854
+
855
+ if len(data) > 1:
856
+ properties, _ = parse_properties(data, 1)
857
+
858
+ return {
859
+ "reason_code": reason_code,
860
+ "properties": properties,
861
+ }
862
+
863
+ def _parse_auth(self, data: bytes) -> dict[str, Any]:
864
+ """Parse AUTH packet (MQTT 5.0 only).
865
+
866
+ Args:
867
+ data: Variable header bytes.
868
+
869
+ Returns:
870
+ Parsed AUTH fields.
871
+
872
+ Raises:
873
+ ValueError: If packet is malformed.
874
+
875
+ Example:
876
+ >>> analyzer = MQTTAnalyzer()
877
+ >>> data = b"\\x00" # Reason code
878
+ >>> result = analyzer._parse_auth(data)
879
+ >>> result["reason_code"]
880
+ 0
881
+ """
882
+ if len(data) < 1:
883
+ raise ValueError("Insufficient data for AUTH")
884
+
885
+ reason_code = data[0]
886
+ properties: dict[str, Any] = {}
887
+
888
+ if len(data) > 1:
889
+ properties, _ = parse_properties(data, 1)
890
+
891
+ return {
892
+ "protocol_version": "5.0",
893
+ "reason_code": reason_code,
894
+ "properties": properties,
895
+ }
896
+
897
+ def _track_session(self, connect_data: dict[str, Any]) -> None:
898
+ """Track MQTT session from CONNECT packet.
899
+
900
+ Args:
901
+ connect_data: Parsed CONNECT packet data.
902
+
903
+ Example:
904
+ >>> analyzer = MQTTAnalyzer()
905
+ >>> connect_data = {
906
+ ... "client_id": "test",
907
+ ... "username": "admin",
908
+ ... "protocol_version": "3.1.1",
909
+ ... "keep_alive": 60,
910
+ ... "flags": {"clean_session": True},
911
+ ... }
912
+ >>> analyzer._track_session(connect_data)
913
+ >>> "test" in analyzer.sessions
914
+ True
915
+ """
916
+ client_id = connect_data["client_id"]
917
+
918
+ session = MQTTSession(
919
+ client_id=client_id,
920
+ username=connect_data.get("username"),
921
+ protocol_version=connect_data.get("protocol_version", "3.1.1"),
922
+ keep_alive=connect_data.get("keep_alive", 60),
923
+ clean_session=connect_data.get("flags", {}).get("clean_session", True),
924
+ will_topic=connect_data.get("will_topic"),
925
+ will_message=connect_data.get("will_message"),
926
+ )
927
+
928
+ self.sessions[client_id] = session
929
+
930
+ def get_topic_hierarchy(self) -> dict[str, Any]:
931
+ """Build hierarchical topic tree from observed topics.
932
+
933
+ Topics are split by '/' separator and organized into nested
934
+ dictionaries representing the topic hierarchy.
935
+
936
+ Returns:
937
+ Nested dictionary representing topic tree.
938
+
939
+ Example:
940
+ >>> analyzer = MQTTAnalyzer()
941
+ >>> analyzer.topics = {"home/sensor/temperature", "home/sensor/humidity"}
942
+ >>> tree = analyzer.get_topic_hierarchy()
943
+ >>> "home" in tree
944
+ True
945
+ >>> "sensor" in tree["home"]
946
+ True
947
+ """
948
+ tree: dict[str, Any] = {}
949
+
950
+ for topic in self.topics:
951
+ parts = topic.split("/")
952
+ current = tree
953
+
954
+ for part in parts:
955
+ if part not in current:
956
+ current[part] = {}
957
+ current = current[part]
958
+
959
+ return tree
960
+
961
+ def export_topology(self, output_path: Path) -> None:
962
+ """Export topic hierarchy and session information as JSON.
963
+
964
+ Args:
965
+ output_path: Path to output JSON file.
966
+
967
+ Example:
968
+ >>> analyzer = MQTTAnalyzer()
969
+ >>> analyzer.topics = {"test/topic"}
970
+ >>> analyzer.export_topology(Path("topology.json"))
971
+ """
972
+ topology = {
973
+ "topic_hierarchy": self.get_topic_hierarchy(),
974
+ "topics": sorted(self.topics),
975
+ "sessions": {
976
+ client_id: {
977
+ "client_id": session.client_id,
978
+ "username": session.username,
979
+ "protocol_version": session.protocol_version,
980
+ "keep_alive": session.keep_alive,
981
+ "clean_session": session.clean_session,
982
+ "will_topic": session.will_topic,
983
+ "subscribed_topics": session.subscribed_topics,
984
+ "published_topics": session.published_topics,
985
+ }
986
+ for client_id, session in self.sessions.items()
987
+ },
988
+ "packet_count": len(self.packets),
989
+ }
990
+
991
+ with output_path.open("w", encoding="utf-8") as f:
992
+ json.dump(topology, f, indent=2)
993
+
994
+
995
+ __all__ = [
996
+ "MQTTAnalyzer",
997
+ "MQTTPacket",
998
+ "MQTTSession",
999
+ ]