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
@@ -118,13 +118,66 @@ def classify_data_type(data: DataType) -> ClassificationResult:
118
118
  >>> result.primary_type
119
119
  'text'
120
120
  """
121
+ data = _normalize_data(data)
122
+ stats = _compute_statistics(data)
123
+
124
+ # Classification logic in priority order
125
+ result = (
126
+ _check_padding(stats)
127
+ or _check_binary_signatures(data, stats)
128
+ or _check_compression_signatures(data, stats)
129
+ or _check_text(stats)
130
+ or _check_encrypted(stats)
131
+ or _check_compressed(stats)
132
+ or _default_binary(stats)
133
+ )
134
+
135
+ return result
136
+
137
+
138
+ def _normalize_data(data: DataType) -> bytes:
139
+ """Normalize input data to bytes.
140
+
141
+ Args:
142
+ data: Input data as bytes, bytearray, or numpy array.
143
+
144
+ Returns:
145
+ Normalized bytes object.
146
+
147
+ Raises:
148
+ ValueError: If data is empty.
149
+ """
150
+ data_bytes: bytes
121
151
  if isinstance(data, np.ndarray):
122
- data = data.tobytes() if data.dtype == np.uint8 else bytes(data.flatten())
152
+ data_bytes = data.tobytes() if data.dtype == np.uint8 else bytes(data.flatten())
153
+ else:
154
+ data_bytes = bytes(data)
123
155
 
124
- if not data:
156
+ if not data_bytes:
125
157
  raise ValueError("Cannot classify empty data")
126
158
 
127
- # Calculate statistics
159
+ return data_bytes
160
+
161
+
162
+ @dataclass
163
+ class _Statistics:
164
+ """Internal statistics for classification."""
165
+
166
+ entropy: float
167
+ printable_ratio: float
168
+ null_ratio: float
169
+ byte_variance: float
170
+
171
+
172
+ def _compute_statistics(data: bytes) -> _Statistics:
173
+ """Compute statistics for classification.
174
+
175
+ Args:
176
+ data: Input data as bytes.
177
+
178
+ Returns:
179
+ Statistics object with computed metrics.
180
+ """
128
181
  entropy_val = shannon_entropy(data)
129
182
 
130
183
  # Printable ASCII: 0x20-0x7E plus tab, newline, carriage return
@@ -139,103 +192,118 @@ def classify_data_type(data: DataType) -> ClassificationResult:
139
192
  byte_array = np.frombuffer(data, dtype=np.uint8)
140
193
  byte_variance = float(np.var(byte_array))
141
194
 
142
- details = {}
195
+ return _Statistics(
196
+ entropy=entropy_val,
197
+ printable_ratio=printable_ratio,
198
+ null_ratio=null_ratio,
199
+ byte_variance=byte_variance,
200
+ )
201
+
143
202
 
144
- # Classification logic
145
- # 1. Padding/null regions
146
- if null_ratio > 0.9:
203
+ def _check_padding(stats: _Statistics) -> ClassificationResult | None:
204
+ """Check if data is padding/null region."""
205
+ if stats.null_ratio > 0.9:
147
206
  return ClassificationResult(
148
207
  primary_type="padding",
149
- confidence=min(1.0, null_ratio),
150
- entropy=entropy_val,
151
- printable_ratio=printable_ratio,
152
- null_ratio=null_ratio,
153
- byte_variance=byte_variance,
208
+ confidence=min(1.0, stats.null_ratio),
209
+ entropy=stats.entropy,
210
+ printable_ratio=stats.printable_ratio,
211
+ null_ratio=stats.null_ratio,
212
+ byte_variance=stats.byte_variance,
154
213
  details={"reason": "high_null_ratio"},
155
214
  )
215
+ return None
156
216
 
157
- # 2. Check for executable/binary signatures (BEFORE compression and encrypted)
217
+
218
+ def _check_binary_signatures(data: bytes, stats: _Statistics) -> ClassificationResult | None:
219
+ """Check for executable/binary signatures."""
158
220
  for sig, bin_type in BINARY_SIGNATURES.items():
159
221
  if data[: len(sig)] == sig:
160
- details["binary_type"] = bin_type
161
222
  return ClassificationResult(
162
223
  primary_type="binary",
163
224
  confidence=0.95,
164
- entropy=entropy_val,
165
- printable_ratio=printable_ratio,
166
- null_ratio=null_ratio,
167
- byte_variance=byte_variance,
168
- details=details,
225
+ entropy=stats.entropy,
226
+ printable_ratio=stats.printable_ratio,
227
+ null_ratio=stats.null_ratio,
228
+ byte_variance=stats.byte_variance,
229
+ details={"binary_type": bin_type},
169
230
  )
231
+ return None
232
+
170
233
 
171
- # 3. Check for compression signatures
234
+ def _check_compression_signatures(data: bytes, stats: _Statistics) -> ClassificationResult | None:
235
+ """Check for compression signatures."""
172
236
  for sig, comp_type in COMPRESSION_SIGNATURES.items():
173
237
  if data[: len(sig)] == sig:
174
- details["compression_type"] = comp_type
175
238
  return ClassificationResult(
176
239
  primary_type="compressed",
177
240
  confidence=0.95,
178
- entropy=entropy_val,
179
- printable_ratio=printable_ratio,
180
- null_ratio=null_ratio,
181
- byte_variance=byte_variance,
182
- details=details,
241
+ entropy=stats.entropy,
242
+ printable_ratio=stats.printable_ratio,
243
+ null_ratio=stats.null_ratio,
244
+ byte_variance=stats.byte_variance,
245
+ details={"compression_type": comp_type},
183
246
  )
247
+ return None
184
248
 
185
- # 4. Text data (high printable ratio) - check BEFORE entropy-based classification
186
- if printable_ratio > 0.75 and entropy_val < 6.5:
187
- confidence = min(1.0, printable_ratio * 0.95)
188
- details["reason"] = "high_printable_ratio"
249
+
250
+ def _check_text(stats: _Statistics) -> ClassificationResult | None:
251
+ """Check for text data (high printable ratio)."""
252
+ if stats.printable_ratio > 0.75 and stats.entropy < 6.5:
253
+ confidence = min(1.0, stats.printable_ratio * 0.95)
189
254
  return ClassificationResult(
190
255
  primary_type="text",
191
256
  confidence=confidence,
192
- entropy=entropy_val,
193
- printable_ratio=printable_ratio,
194
- null_ratio=null_ratio,
195
- byte_variance=byte_variance,
196
- details=details,
257
+ entropy=stats.entropy,
258
+ printable_ratio=stats.printable_ratio,
259
+ null_ratio=stats.null_ratio,
260
+ byte_variance=stats.byte_variance,
261
+ details={"reason": "high_printable_ratio"},
197
262
  )
263
+ return None
264
+
198
265
 
199
- # 5. Encrypted/random data (high entropy, no structure)
200
- if entropy_val > 7.5 and byte_variance > 5000:
201
- # High entropy with high variance suggests random/encrypted
202
- confidence = min(1.0, (entropy_val - 7.5) / 0.5 + 0.7)
203
- details["reason"] = "high_entropy_and_variance"
266
+ def _check_encrypted(stats: _Statistics) -> ClassificationResult | None:
267
+ """Check for encrypted/random data (high entropy, no structure)."""
268
+ if stats.entropy > 7.5 and stats.byte_variance > 5000:
269
+ confidence = min(1.0, (stats.entropy - 7.5) / 0.5 + 0.7)
204
270
  return ClassificationResult(
205
271
  primary_type="encrypted",
206
272
  confidence=confidence,
207
- entropy=entropy_val,
208
- printable_ratio=printable_ratio,
209
- null_ratio=null_ratio,
210
- byte_variance=byte_variance,
211
- details=details,
273
+ entropy=stats.entropy,
274
+ printable_ratio=stats.printable_ratio,
275
+ null_ratio=stats.null_ratio,
276
+ byte_variance=stats.byte_variance,
277
+ details={"reason": "high_entropy_and_variance"},
212
278
  )
279
+ return None
280
+
213
281
 
214
- # 6. Compressed data (high entropy, some structure)
215
- if 6.5 <= entropy_val <= 7.5:
216
- confidence = 0.7
217
- details["reason"] = "compression_entropy_range"
282
+ def _check_compressed(stats: _Statistics) -> ClassificationResult | None:
283
+ """Check for compressed data (high entropy, some structure)."""
284
+ if 6.5 <= stats.entropy <= 7.5:
218
285
  return ClassificationResult(
219
286
  primary_type="compressed",
220
- confidence=confidence,
221
- entropy=entropy_val,
222
- printable_ratio=printable_ratio,
223
- null_ratio=null_ratio,
224
- byte_variance=byte_variance,
225
- details=details,
287
+ confidence=0.7,
288
+ entropy=stats.entropy,
289
+ printable_ratio=stats.printable_ratio,
290
+ null_ratio=stats.null_ratio,
291
+ byte_variance=stats.byte_variance,
292
+ details={"reason": "compression_entropy_range"},
226
293
  )
294
+ return None
295
+
227
296
 
228
- # 7. Default to binary/structured
229
- confidence = 0.6
230
- details["reason"] = "default_binary"
297
+ def _default_binary(stats: _Statistics) -> ClassificationResult:
298
+ """Default to binary/structured classification."""
231
299
  return ClassificationResult(
232
300
  primary_type="binary",
233
- confidence=confidence,
234
- entropy=entropy_val,
235
- printable_ratio=printable_ratio,
236
- null_ratio=null_ratio,
237
- byte_variance=byte_variance,
238
- details=details,
301
+ confidence=0.6,
302
+ entropy=stats.entropy,
303
+ printable_ratio=stats.printable_ratio,
304
+ null_ratio=stats.null_ratio,
305
+ byte_variance=stats.byte_variance,
306
+ details={"reason": "default_binary"},
239
307
  )
240
308
 
241
309
 
@@ -262,65 +330,100 @@ def detect_text_regions(
262
330
  >>> len(regions) > 0
263
331
  True
264
332
  """
333
+ # Convert numpy arrays to bytes
334
+ data_bytes: bytes
265
335
  if isinstance(data, np.ndarray):
266
- data = data.tobytes() if data.dtype == np.uint8 else bytes(data.flatten())
336
+ data_bytes = data.tobytes() if data.dtype == np.uint8 else bytes(data.flatten())
337
+ else:
338
+ data_bytes = bytes(data)
267
339
 
268
- regions = []
340
+ regions: list[RegionClassification] = []
269
341
  in_region = False
270
342
  region_start = 0
271
- _printable_in_window = 0
272
343
  window_size = min_length
273
344
 
274
- for i, byte in enumerate(data):
275
- _is_printable = 32 <= byte <= 126 or byte in (9, 10, 13)
276
-
345
+ for i in range(len(data_bytes)):
277
346
  if not in_region:
278
347
  # Look for start of text region
279
- if i >= window_size - 1:
280
- # Check window
281
- window = data[i - window_size + 1 : i + 1]
282
- printable_count = sum(1 for b in window if 32 <= b <= 126 or b in (9, 10, 13))
283
- if printable_count / window_size >= min_printable:
284
- in_region = True
285
- region_start = i - window_size + 1
348
+ start_pos = _check_region_start(data_bytes, i, window_size, min_printable)
349
+ if start_pos is not None:
350
+ in_region = True
351
+ region_start = start_pos
286
352
  else:
287
- # In text region, look for end
288
- # Use a sliding window to detect when printable ratio drops
289
- if i >= region_start + window_size:
290
- window = data[i - window_size + 1 : i + 1]
291
- printable_count = sum(1 for b in window if 32 <= b <= 126 or b in (9, 10, 13))
292
- if printable_count / window_size < min_printable:
293
- # End of region
294
- region_data = data[region_start : i - window_size + 1]
295
- if len(region_data) >= min_length:
296
- classification = classify_data_type(region_data)
297
- regions.append(
298
- RegionClassification(
299
- start=region_start,
300
- end=i - window_size + 1,
301
- length=len(region_data),
302
- classification=classification,
303
- )
304
- )
305
- in_region = False
353
+ # Look for end of text region
354
+ if _check_region_end(data_bytes, i, region_start, window_size, min_printable):
355
+ _append_region(data_bytes, regions, region_start, i - window_size + 1, min_length)
356
+ in_region = False
306
357
 
307
358
  # Handle region extending to end
308
359
  if in_region:
309
- region_data = data[region_start:]
310
- if len(region_data) >= min_length:
311
- classification = classify_data_type(region_data)
312
- regions.append(
313
- RegionClassification(
314
- start=region_start,
315
- end=len(data),
316
- length=len(region_data),
317
- classification=classification,
318
- )
319
- )
360
+ _append_region(data_bytes, regions, region_start, len(data_bytes), min_length)
320
361
 
321
362
  return regions
322
363
 
323
364
 
365
+ def _is_printable_byte(byte: int) -> bool:
366
+ """Check if byte is printable ASCII."""
367
+ return 32 <= byte <= 126 or byte in (9, 10, 13)
368
+
369
+
370
+ def _check_region_start(
371
+ data: bytes,
372
+ i: int,
373
+ window_size: int,
374
+ min_printable: float,
375
+ ) -> int | None:
376
+ """Check if position marks start of text region."""
377
+ if i < window_size - 1:
378
+ return None
379
+
380
+ window = data[i - window_size + 1 : i + 1]
381
+ printable_count = sum(1 for b in window if _is_printable_byte(b))
382
+
383
+ if printable_count / window_size >= min_printable:
384
+ return i - window_size + 1
385
+
386
+ return None
387
+
388
+
389
+ def _check_region_end(
390
+ data: bytes,
391
+ i: int,
392
+ region_start: int,
393
+ window_size: int,
394
+ min_printable: float,
395
+ ) -> bool:
396
+ """Check if position marks end of text region."""
397
+ if i < region_start + window_size:
398
+ return False
399
+
400
+ window = data[i - window_size + 1 : i + 1]
401
+ printable_count = sum(1 for b in window if _is_printable_byte(b))
402
+
403
+ return printable_count / window_size < min_printable
404
+
405
+
406
+ def _append_region(
407
+ data: bytes,
408
+ regions: list[RegionClassification],
409
+ start: int,
410
+ end: int,
411
+ min_length: int,
412
+ ) -> None:
413
+ """Append region to list if it meets minimum length."""
414
+ region_data = data[start:end]
415
+ if len(region_data) >= min_length:
416
+ classification = classify_data_type(region_data)
417
+ regions.append(
418
+ RegionClassification(
419
+ start=start,
420
+ end=end,
421
+ length=len(region_data),
422
+ classification=classification,
423
+ )
424
+ )
425
+
426
+
324
427
  def detect_encrypted_regions(
325
428
  data: DataType, min_length: int = 64, min_entropy: float = 7.5
326
429
  ) -> list[RegionClassification]:
@@ -356,6 +459,13 @@ def detect_encrypted_regions(
356
459
  window_size = min_length
357
460
  step = window_size // 4
358
461
 
462
+ # Validate step to prevent infinite loop
463
+ if step <= 0:
464
+ raise ValueError(
465
+ f"Invalid step size {step} (window_size={window_size}). "
466
+ "window_size must be at least 4 to produce positive step."
467
+ )
468
+
359
469
  i = 0
360
470
  while i < len(data) - window_size:
361
471
  window = data[i : i + window_size]
@@ -410,6 +520,9 @@ def detect_compressed_regions(data: DataType, min_length: int = 64) -> list[Regi
410
520
  Returns:
411
521
  List of detected compressed regions
412
522
 
523
+ Raises:
524
+ ValueError: If detected region exceeds MAX_REGION_SIZE (100MB).
525
+
413
526
  Example:
414
527
  >>> import gzip
415
528
  >>> compressed = gzip.compress(b'Hello World' * 100)
@@ -417,6 +530,8 @@ def detect_compressed_regions(data: DataType, min_length: int = 64) -> list[Regi
417
530
  >>> len(regions) > 0
418
531
  True
419
532
  """
533
+ MAX_REGION_SIZE = 100 * 1024 * 1024 # 100MB limit
534
+
420
535
  if isinstance(data, np.ndarray):
421
536
  data = data.tobytes() if data.dtype == np.uint8 else bytes(data.flatten())
422
537
 
@@ -438,6 +553,13 @@ def detect_compressed_regions(data: DataType, min_length: int = 64) -> list[Regi
438
553
  # Extend based on high entropy
439
554
  window_size = 256
440
555
  while region_end < len(data):
556
+ # Safety check: prevent unbounded region growth
557
+ if region_end - region_start >= MAX_REGION_SIZE:
558
+ raise ValueError(
559
+ f"Compressed region size exceeded {MAX_REGION_SIZE // (1024 * 1024)}MB limit. "
560
+ f"This may indicate malformed data or incorrect compression signature detection."
561
+ )
562
+
441
563
  window = data[region_end : region_end + window_size]
442
564
  if len(window) < window_size:
443
565
  break
@@ -396,25 +396,34 @@ def _detect_transitions_boundary_scan(
396
396
  Returns:
397
397
  List of detected transitions
398
398
  """
399
- data_len = len(data)
399
+ region_size = _determine_boundary_scan_region_size(len(data), window)
400
+ if region_size < 4:
401
+ return []
400
402
 
401
- # Region size for comparison - use window or adaptive size
402
- region_size = min(window, data_len // 3)
403
- if region_size < 8:
404
- region_size = max(8, data_len // 4)
403
+ scan_start, scan_end = _compute_boundary_scan_range(len(data), region_size)
404
+ if scan_start >= scan_end:
405
+ return []
405
406
 
406
- if region_size < 4:
407
+ best_transition = _find_best_boundary_transition(
408
+ data, region_size, scan_start, scan_end, threshold, min_gap
409
+ )
410
+
411
+ if best_transition is None:
407
412
  return []
408
413
 
409
- transitions = []
410
- last_offset = -min_gap - 1
414
+ return _accumulate_all_boundary_transitions(data, best_transition, window, threshold, min_gap)
415
+
416
+
417
+ def _determine_boundary_scan_region_size(data_len: int, window: int) -> int:
418
+ """Determine region size for boundary scanning."""
419
+ region_size = min(window, data_len // 3)
420
+ if region_size < 8:
421
+ region_size = max(8, data_len // 4)
422
+ return region_size
411
423
 
412
- # Track best transition found
413
- best_transition = None
414
- best_delta = 0.0
415
424
 
416
- # Scan potential boundary points
417
- # We need at least region_size bytes on each side
425
+ def _compute_boundary_scan_range(data_len: int, region_size: int) -> tuple[int, int]:
426
+ """Compute scan range for boundary detection."""
418
427
  scan_start = region_size
419
428
  scan_end = data_len - region_size
420
429
 
@@ -424,16 +433,25 @@ def _detect_transitions_boundary_scan(
424
433
  scan_start = region_size
425
434
  scan_end = data_len - region_size
426
435
 
427
- if scan_start >= scan_end:
428
- return []
436
+ return scan_start, scan_end
429
437
 
430
- # Use a step size to avoid scanning every byte
438
+
439
+ def _find_best_boundary_transition(
440
+ data: bytes,
441
+ region_size: int,
442
+ scan_start: int,
443
+ scan_end: int,
444
+ threshold: float,
445
+ min_gap: int,
446
+ ) -> EntropyTransition | None:
447
+ """Find the strongest boundary transition in scan range."""
448
+ best_transition = None
449
+ best_delta = 0.0
450
+ last_offset = -min_gap - 1
431
451
  scan_step = max(1, region_size // 4)
432
452
 
433
453
  for offset in range(scan_start, scan_end + 1, scan_step):
434
- # Compute entropy of region BEFORE this point
435
454
  region_before = data[offset - region_size : offset]
436
- # Compute entropy of region AFTER this point
437
455
  region_after = data[offset : offset + region_size]
438
456
 
439
457
  if len(region_before) < 4 or len(region_after) < 4:
@@ -448,43 +466,50 @@ def _detect_transitions_boundary_scan(
448
466
  delta = entropy_after - entropy_before
449
467
 
450
468
  # Track the strongest transition that exceeds threshold
451
- if abs(delta) >= threshold:
452
- # Check min_gap constraint
453
- if offset - last_offset >= min_gap:
454
- if abs(delta) > abs(best_delta):
455
- best_delta = delta
456
- best_transition = EntropyTransition(
457
- offset=offset,
458
- entropy_before=entropy_before,
459
- entropy_after=entropy_after,
460
- delta=delta,
461
- transition_type="low_to_high" if delta > 0 else "high_to_low",
462
- )
469
+ if abs(delta) >= threshold and offset - last_offset >= min_gap:
470
+ if abs(delta) > abs(best_delta):
471
+ best_delta = delta
472
+ best_transition = EntropyTransition(
473
+ offset=offset,
474
+ entropy_before=entropy_before,
475
+ entropy_after=entropy_after,
476
+ delta=delta,
477
+ transition_type="low_to_high" if delta > 0 else "high_to_low",
478
+ )
479
+
480
+ return best_transition
481
+
482
+
483
+ def _accumulate_all_boundary_transitions(
484
+ data: bytes,
485
+ best_transition: EntropyTransition,
486
+ window: int,
487
+ threshold: float,
488
+ min_gap: int,
489
+ ) -> list[EntropyTransition]:
490
+ """Accumulate all boundary transitions including recursive finds."""
491
+ transitions = [best_transition]
492
+ last_offset = best_transition.offset
493
+
494
+ # Continue scanning for more transitions after this one
495
+ remaining_transitions = _detect_transitions_boundary_scan(
496
+ data[best_transition.offset :],
497
+ window,
498
+ threshold,
499
+ min_gap,
500
+ )
463
501
 
464
- if best_transition is not None:
465
- transitions.append(best_transition)
466
- last_offset = best_transition.offset
467
-
468
- # Continue scanning for more transitions after this one
469
- # (for data with multiple transitions)
470
- remaining_transitions = _detect_transitions_boundary_scan(
471
- data[best_transition.offset :],
472
- window,
473
- threshold,
474
- min_gap,
502
+ for t in remaining_transitions:
503
+ adjusted_t = EntropyTransition(
504
+ offset=t.offset + best_transition.offset,
505
+ entropy_before=t.entropy_before,
506
+ entropy_after=t.entropy_after,
507
+ delta=t.delta,
508
+ transition_type=t.transition_type,
475
509
  )
476
- for t in remaining_transitions:
477
- # Adjust offset to be relative to original data
478
- adjusted_t = EntropyTransition(
479
- offset=t.offset + best_transition.offset,
480
- entropy_before=t.entropy_before,
481
- entropy_after=t.entropy_after,
482
- delta=t.delta,
483
- transition_type=t.transition_type,
484
- )
485
- if adjusted_t.offset - last_offset >= min_gap:
486
- transitions.append(adjusted_t)
487
- last_offset = adjusted_t.offset
510
+ if adjusted_t.offset - last_offset >= min_gap:
511
+ transitions.append(adjusted_t)
512
+ last_offset = adjusted_t.offset
488
513
 
489
514
  return transitions
490
515