oscura 0.5.1__py3-none-any.whl → 0.7.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.
- oscura/__init__.py +169 -167
- oscura/analyzers/__init__.py +3 -0
- oscura/analyzers/classification.py +659 -0
- oscura/analyzers/digital/edges.py +325 -65
- oscura/analyzers/digital/quality.py +293 -166
- oscura/analyzers/digital/timing.py +260 -115
- oscura/analyzers/digital/timing_numba.py +334 -0
- oscura/analyzers/entropy.py +605 -0
- oscura/analyzers/eye/diagram.py +176 -109
- oscura/analyzers/eye/metrics.py +5 -5
- oscura/analyzers/jitter/__init__.py +6 -4
- oscura/analyzers/jitter/ber.py +52 -52
- oscura/analyzers/jitter/classification.py +156 -0
- oscura/analyzers/jitter/decomposition.py +163 -113
- oscura/analyzers/jitter/spectrum.py +80 -64
- oscura/analyzers/ml/__init__.py +39 -0
- oscura/analyzers/ml/features.py +600 -0
- oscura/analyzers/ml/signal_classifier.py +604 -0
- oscura/analyzers/packet/daq.py +246 -158
- oscura/analyzers/packet/parser.py +12 -1
- oscura/analyzers/packet/payload.py +50 -2110
- oscura/analyzers/packet/payload_analysis.py +361 -181
- oscura/analyzers/packet/payload_patterns.py +133 -70
- oscura/analyzers/packet/stream.py +84 -23
- oscura/analyzers/patterns/__init__.py +26 -5
- oscura/analyzers/patterns/anomaly_detection.py +908 -0
- oscura/analyzers/patterns/clustering.py +169 -108
- oscura/analyzers/patterns/clustering_optimized.py +227 -0
- oscura/analyzers/patterns/discovery.py +1 -1
- oscura/analyzers/patterns/matching.py +581 -197
- oscura/analyzers/patterns/pattern_mining.py +778 -0
- oscura/analyzers/patterns/periodic.py +121 -38
- oscura/analyzers/patterns/sequences.py +175 -78
- oscura/analyzers/power/conduction.py +1 -1
- oscura/analyzers/power/soa.py +6 -6
- oscura/analyzers/power/switching.py +250 -110
- oscura/analyzers/protocol/__init__.py +17 -1
- oscura/analyzers/protocols/base.py +6 -6
- oscura/analyzers/protocols/ble/__init__.py +38 -0
- oscura/analyzers/protocols/ble/analyzer.py +809 -0
- oscura/analyzers/protocols/ble/uuids.py +288 -0
- oscura/analyzers/protocols/can.py +257 -127
- oscura/analyzers/protocols/can_fd.py +107 -80
- oscura/analyzers/protocols/flexray.py +139 -80
- oscura/analyzers/protocols/hdlc.py +93 -58
- oscura/analyzers/protocols/i2c.py +247 -106
- oscura/analyzers/protocols/i2s.py +138 -86
- oscura/analyzers/protocols/industrial/__init__.py +40 -0
- oscura/analyzers/protocols/industrial/bacnet/__init__.py +33 -0
- oscura/analyzers/protocols/industrial/bacnet/analyzer.py +708 -0
- oscura/analyzers/protocols/industrial/bacnet/encoding.py +412 -0
- oscura/analyzers/protocols/industrial/bacnet/services.py +622 -0
- oscura/analyzers/protocols/industrial/ethercat/__init__.py +30 -0
- oscura/analyzers/protocols/industrial/ethercat/analyzer.py +474 -0
- oscura/analyzers/protocols/industrial/ethercat/mailbox.py +339 -0
- oscura/analyzers/protocols/industrial/ethercat/topology.py +166 -0
- oscura/analyzers/protocols/industrial/modbus/__init__.py +31 -0
- oscura/analyzers/protocols/industrial/modbus/analyzer.py +525 -0
- oscura/analyzers/protocols/industrial/modbus/crc.py +79 -0
- oscura/analyzers/protocols/industrial/modbus/functions.py +436 -0
- oscura/analyzers/protocols/industrial/opcua/__init__.py +21 -0
- oscura/analyzers/protocols/industrial/opcua/analyzer.py +552 -0
- oscura/analyzers/protocols/industrial/opcua/datatypes.py +446 -0
- oscura/analyzers/protocols/industrial/opcua/services.py +264 -0
- oscura/analyzers/protocols/industrial/profinet/__init__.py +23 -0
- oscura/analyzers/protocols/industrial/profinet/analyzer.py +441 -0
- oscura/analyzers/protocols/industrial/profinet/dcp.py +263 -0
- oscura/analyzers/protocols/industrial/profinet/ptcp.py +200 -0
- oscura/analyzers/protocols/jtag.py +180 -98
- oscura/analyzers/protocols/lin.py +219 -114
- oscura/analyzers/protocols/manchester.py +4 -4
- oscura/analyzers/protocols/onewire.py +253 -149
- oscura/analyzers/protocols/parallel_bus/__init__.py +20 -0
- oscura/analyzers/protocols/parallel_bus/centronics.py +92 -0
- oscura/analyzers/protocols/parallel_bus/gpib.py +137 -0
- oscura/analyzers/protocols/spi.py +192 -95
- oscura/analyzers/protocols/swd.py +321 -167
- oscura/analyzers/protocols/uart.py +267 -125
- oscura/analyzers/protocols/usb.py +235 -131
- oscura/analyzers/side_channel/power.py +17 -12
- oscura/analyzers/signal/__init__.py +15 -0
- oscura/analyzers/signal/timing_analysis.py +1086 -0
- oscura/analyzers/signal_integrity/__init__.py +4 -1
- oscura/analyzers/signal_integrity/sparams.py +2 -19
- oscura/analyzers/spectral/chunked.py +129 -60
- oscura/analyzers/spectral/chunked_fft.py +300 -94
- oscura/analyzers/spectral/chunked_wavelet.py +100 -80
- oscura/analyzers/statistical/checksum.py +376 -217
- oscura/analyzers/statistical/classification.py +229 -107
- oscura/analyzers/statistical/entropy.py +78 -53
- oscura/analyzers/statistics/correlation.py +407 -211
- oscura/analyzers/statistics/outliers.py +2 -2
- oscura/analyzers/statistics/streaming.py +30 -5
- oscura/analyzers/validation.py +216 -101
- oscura/analyzers/waveform/measurements.py +9 -0
- oscura/analyzers/waveform/measurements_with_uncertainty.py +31 -15
- oscura/analyzers/waveform/spectral.py +500 -228
- oscura/api/__init__.py +31 -5
- oscura/api/dsl/__init__.py +582 -0
- oscura/{dsl → api/dsl}/commands.py +43 -76
- oscura/{dsl → api/dsl}/interpreter.py +26 -51
- oscura/{dsl → api/dsl}/parser.py +107 -77
- oscura/{dsl → api/dsl}/repl.py +2 -2
- oscura/api/dsl.py +1 -1
- oscura/{integrations → api/integrations}/__init__.py +1 -1
- oscura/{integrations → api/integrations}/llm.py +201 -102
- oscura/api/operators.py +3 -3
- oscura/api/optimization.py +144 -30
- oscura/api/rest_server.py +921 -0
- oscura/api/server/__init__.py +17 -0
- oscura/api/server/dashboard.py +850 -0
- oscura/api/server/static/README.md +34 -0
- oscura/api/server/templates/base.html +181 -0
- oscura/api/server/templates/export.html +120 -0
- oscura/api/server/templates/home.html +284 -0
- oscura/api/server/templates/protocols.html +58 -0
- oscura/api/server/templates/reports.html +43 -0
- oscura/api/server/templates/session_detail.html +89 -0
- oscura/api/server/templates/sessions.html +83 -0
- oscura/api/server/templates/waveforms.html +73 -0
- oscura/automotive/__init__.py +8 -1
- oscura/automotive/can/__init__.py +10 -0
- oscura/automotive/can/checksum.py +3 -1
- oscura/automotive/can/dbc_generator.py +590 -0
- oscura/automotive/can/message_wrapper.py +121 -74
- oscura/automotive/can/patterns.py +98 -21
- oscura/automotive/can/session.py +292 -56
- oscura/automotive/can/state_machine.py +6 -3
- oscura/automotive/can/stimulus_response.py +97 -75
- oscura/automotive/dbc/__init__.py +10 -2
- oscura/automotive/dbc/generator.py +84 -56
- oscura/automotive/dbc/parser.py +6 -6
- oscura/automotive/dtc/data.json +17 -102
- oscura/automotive/dtc/database.py +2 -2
- oscura/automotive/flexray/__init__.py +31 -0
- oscura/automotive/flexray/analyzer.py +504 -0
- oscura/automotive/flexray/crc.py +185 -0
- oscura/automotive/flexray/fibex.py +449 -0
- oscura/automotive/j1939/__init__.py +45 -8
- oscura/automotive/j1939/analyzer.py +605 -0
- oscura/automotive/j1939/spns.py +326 -0
- oscura/automotive/j1939/transport.py +306 -0
- oscura/automotive/lin/__init__.py +47 -0
- oscura/automotive/lin/analyzer.py +612 -0
- oscura/automotive/loaders/blf.py +13 -2
- oscura/automotive/loaders/csv_can.py +143 -72
- oscura/automotive/loaders/dispatcher.py +50 -2
- oscura/automotive/loaders/mdf.py +86 -45
- oscura/automotive/loaders/pcap.py +111 -61
- oscura/automotive/uds/__init__.py +4 -0
- oscura/automotive/uds/analyzer.py +725 -0
- oscura/automotive/uds/decoder.py +140 -58
- oscura/automotive/uds/models.py +7 -1
- oscura/automotive/visualization.py +1 -1
- oscura/cli/analyze.py +348 -0
- oscura/cli/batch.py +142 -122
- oscura/cli/benchmark.py +275 -0
- oscura/cli/characterize.py +137 -82
- oscura/cli/compare.py +224 -131
- oscura/cli/completion.py +250 -0
- oscura/cli/config_cmd.py +361 -0
- oscura/cli/decode.py +164 -87
- oscura/cli/export.py +286 -0
- oscura/cli/main.py +115 -31
- oscura/{onboarding → cli/onboarding}/__init__.py +3 -3
- oscura/{onboarding → cli/onboarding}/help.py +80 -58
- oscura/{onboarding → cli/onboarding}/tutorials.py +97 -72
- oscura/{onboarding → cli/onboarding}/wizard.py +55 -36
- oscura/cli/progress.py +147 -0
- oscura/cli/shell.py +157 -135
- oscura/cli/validate_cmd.py +204 -0
- oscura/cli/visualize.py +158 -0
- oscura/convenience.py +125 -79
- oscura/core/__init__.py +4 -2
- oscura/core/backend_selector.py +3 -3
- oscura/core/cache.py +126 -15
- oscura/core/cancellation.py +1 -1
- oscura/{config → core/config}/__init__.py +20 -11
- oscura/{config → core/config}/defaults.py +1 -1
- oscura/{config → core/config}/loader.py +7 -5
- oscura/{config → core/config}/memory.py +5 -5
- oscura/{config → core/config}/migration.py +1 -1
- oscura/{config → core/config}/pipeline.py +99 -23
- oscura/{config → core/config}/preferences.py +1 -1
- oscura/{config → core/config}/protocol.py +3 -3
- oscura/{config → core/config}/schema.py +426 -272
- oscura/{config → core/config}/settings.py +1 -1
- oscura/{config → core/config}/thresholds.py +195 -153
- oscura/core/correlation.py +5 -6
- oscura/core/cross_domain.py +0 -2
- oscura/core/debug.py +9 -5
- oscura/{extensibility → core/extensibility}/docs.py +158 -70
- oscura/{extensibility → core/extensibility}/extensions.py +160 -76
- oscura/{extensibility → core/extensibility}/logging.py +1 -1
- oscura/{extensibility → core/extensibility}/measurements.py +1 -1
- oscura/{extensibility → core/extensibility}/plugins.py +1 -1
- oscura/{extensibility → core/extensibility}/templates.py +73 -3
- oscura/{extensibility → core/extensibility}/validation.py +1 -1
- oscura/core/gpu_backend.py +11 -7
- oscura/core/log_query.py +101 -11
- oscura/core/logging.py +126 -54
- oscura/core/logging_advanced.py +5 -5
- oscura/core/memory_limits.py +108 -70
- oscura/core/memory_monitor.py +2 -2
- oscura/core/memory_progress.py +7 -7
- oscura/core/memory_warnings.py +1 -1
- oscura/core/numba_backend.py +13 -13
- oscura/{plugins → core/plugins}/__init__.py +9 -9
- oscura/{plugins → core/plugins}/base.py +7 -7
- oscura/{plugins → core/plugins}/cli.py +3 -3
- oscura/{plugins → core/plugins}/discovery.py +186 -106
- oscura/{plugins → core/plugins}/lifecycle.py +1 -1
- oscura/{plugins → core/plugins}/manager.py +7 -7
- oscura/{plugins → core/plugins}/registry.py +3 -3
- oscura/{plugins → core/plugins}/versioning.py +1 -1
- oscura/core/progress.py +16 -1
- oscura/core/provenance.py +8 -2
- oscura/{schemas → core/schemas}/__init__.py +2 -2
- oscura/{schemas → core/schemas}/device_mapping.json +2 -8
- oscura/{schemas → core/schemas}/packet_format.json +4 -24
- oscura/{schemas → core/schemas}/protocol_definition.json +2 -12
- oscura/core/types.py +4 -0
- oscura/core/uncertainty.py +3 -3
- oscura/correlation/__init__.py +52 -0
- oscura/correlation/multi_protocol.py +811 -0
- oscura/discovery/auto_decoder.py +117 -35
- oscura/discovery/comparison.py +191 -86
- oscura/discovery/quality_validator.py +155 -68
- oscura/discovery/signal_detector.py +196 -79
- oscura/export/__init__.py +18 -8
- oscura/export/kaitai_struct.py +513 -0
- oscura/export/scapy_layer.py +801 -0
- oscura/export/wireshark/generator.py +1 -1
- oscura/export/wireshark/templates/dissector.lua.j2 +2 -2
- oscura/export/wireshark_dissector.py +746 -0
- oscura/guidance/wizard.py +207 -111
- oscura/hardware/__init__.py +19 -0
- oscura/{acquisition → hardware/acquisition}/__init__.py +4 -4
- oscura/{acquisition → hardware/acquisition}/file.py +2 -2
- oscura/{acquisition → hardware/acquisition}/hardware.py +7 -7
- oscura/{acquisition → hardware/acquisition}/saleae.py +15 -12
- oscura/{acquisition → hardware/acquisition}/socketcan.py +1 -1
- oscura/{acquisition → hardware/acquisition}/streaming.py +2 -2
- oscura/{acquisition → hardware/acquisition}/synthetic.py +3 -3
- oscura/{acquisition → hardware/acquisition}/visa.py +33 -11
- oscura/hardware/firmware/__init__.py +29 -0
- oscura/hardware/firmware/pattern_recognition.py +874 -0
- oscura/hardware/hal_detector.py +736 -0
- oscura/hardware/security/__init__.py +37 -0
- oscura/hardware/security/side_channel_detector.py +1126 -0
- oscura/inference/__init__.py +4 -0
- oscura/inference/active_learning/observation_table.py +4 -1
- oscura/inference/alignment.py +216 -123
- oscura/inference/bayesian.py +113 -33
- oscura/inference/crc_reverse.py +101 -55
- oscura/inference/logic.py +6 -2
- oscura/inference/message_format.py +342 -183
- oscura/inference/protocol.py +95 -44
- oscura/inference/protocol_dsl.py +180 -82
- oscura/inference/signal_intelligence.py +1439 -706
- oscura/inference/spectral.py +99 -57
- oscura/inference/state_machine.py +810 -158
- oscura/inference/stream.py +270 -110
- oscura/iot/__init__.py +34 -0
- oscura/iot/coap/__init__.py +32 -0
- oscura/iot/coap/analyzer.py +668 -0
- oscura/iot/coap/options.py +212 -0
- oscura/iot/lorawan/__init__.py +21 -0
- oscura/iot/lorawan/crypto.py +206 -0
- oscura/iot/lorawan/decoder.py +801 -0
- oscura/iot/lorawan/mac_commands.py +341 -0
- oscura/iot/mqtt/__init__.py +27 -0
- oscura/iot/mqtt/analyzer.py +999 -0
- oscura/iot/mqtt/properties.py +315 -0
- oscura/iot/zigbee/__init__.py +31 -0
- oscura/iot/zigbee/analyzer.py +615 -0
- oscura/iot/zigbee/security.py +153 -0
- oscura/iot/zigbee/zcl.py +349 -0
- oscura/jupyter/display.py +125 -45
- oscura/{exploratory → jupyter/exploratory}/__init__.py +8 -8
- oscura/{exploratory → jupyter/exploratory}/error_recovery.py +298 -141
- oscura/jupyter/exploratory/fuzzy.py +746 -0
- oscura/{exploratory → jupyter/exploratory}/fuzzy_advanced.py +258 -100
- oscura/{exploratory → jupyter/exploratory}/legacy.py +464 -242
- oscura/{exploratory → jupyter/exploratory}/parse.py +167 -145
- oscura/{exploratory → jupyter/exploratory}/recovery.py +119 -87
- oscura/jupyter/exploratory/sync.py +612 -0
- oscura/{exploratory → jupyter/exploratory}/unknown.py +299 -176
- oscura/jupyter/magic.py +4 -4
- oscura/{ui → jupyter/ui}/__init__.py +2 -2
- oscura/{ui → jupyter/ui}/formatters.py +3 -3
- oscura/{ui → jupyter/ui}/progressive_display.py +153 -82
- oscura/loaders/__init__.py +183 -67
- oscura/loaders/binary.py +88 -1
- oscura/loaders/chipwhisperer.py +153 -137
- oscura/loaders/configurable.py +208 -86
- oscura/loaders/csv_loader.py +458 -215
- oscura/loaders/hdf5_loader.py +278 -119
- oscura/loaders/lazy.py +87 -54
- oscura/loaders/mmap_loader.py +1 -1
- oscura/loaders/numpy_loader.py +253 -116
- oscura/loaders/pcap.py +226 -151
- oscura/loaders/rigol.py +110 -49
- oscura/loaders/sigrok.py +201 -78
- oscura/loaders/tdms.py +81 -58
- oscura/loaders/tektronix.py +291 -174
- oscura/loaders/touchstone.py +182 -87
- oscura/loaders/tss.py +456 -0
- oscura/loaders/vcd.py +215 -117
- oscura/loaders/wav.py +155 -68
- oscura/reporting/__init__.py +9 -0
- oscura/reporting/analyze.py +352 -146
- oscura/reporting/argument_preparer.py +69 -14
- oscura/reporting/auto_report.py +97 -61
- oscura/reporting/batch.py +131 -58
- oscura/reporting/chart_selection.py +57 -45
- oscura/reporting/comparison.py +63 -17
- oscura/reporting/content/executive.py +76 -24
- oscura/reporting/core_formats/multi_format.py +11 -8
- oscura/reporting/engine.py +312 -158
- oscura/reporting/enhanced_reports.py +949 -0
- oscura/reporting/export.py +86 -43
- oscura/reporting/formatting/numbers.py +69 -42
- oscura/reporting/html.py +139 -58
- oscura/reporting/index.py +137 -65
- oscura/reporting/output.py +158 -67
- oscura/reporting/pdf.py +67 -102
- oscura/reporting/plots.py +191 -112
- oscura/reporting/sections.py +88 -47
- oscura/reporting/standards.py +104 -61
- oscura/reporting/summary_generator.py +75 -55
- oscura/reporting/tables.py +138 -54
- oscura/reporting/templates/enhanced/protocol_re.html +525 -0
- oscura/sessions/__init__.py +14 -23
- oscura/sessions/base.py +3 -3
- oscura/sessions/blackbox.py +106 -10
- oscura/sessions/generic.py +2 -2
- oscura/sessions/legacy.py +783 -0
- oscura/side_channel/__init__.py +63 -0
- oscura/side_channel/dpa.py +1025 -0
- oscura/utils/__init__.py +15 -1
- oscura/utils/bitwise.py +118 -0
- oscura/{builders → utils/builders}/__init__.py +1 -1
- oscura/{comparison → utils/comparison}/__init__.py +6 -6
- oscura/{comparison → utils/comparison}/compare.py +202 -101
- oscura/{comparison → utils/comparison}/golden.py +83 -63
- oscura/{comparison → utils/comparison}/limits.py +313 -89
- oscura/{comparison → utils/comparison}/mask.py +151 -45
- oscura/{comparison → utils/comparison}/trace_diff.py +1 -1
- oscura/{comparison → utils/comparison}/visualization.py +147 -89
- oscura/{component → utils/component}/__init__.py +3 -3
- oscura/{component → utils/component}/impedance.py +122 -58
- oscura/{component → utils/component}/reactive.py +165 -168
- oscura/{component → utils/component}/transmission_line.py +3 -3
- oscura/{filtering → utils/filtering}/__init__.py +6 -6
- oscura/{filtering → utils/filtering}/base.py +1 -1
- oscura/{filtering → utils/filtering}/convenience.py +2 -2
- oscura/{filtering → utils/filtering}/design.py +169 -93
- oscura/{filtering → utils/filtering}/filters.py +2 -2
- oscura/{filtering → utils/filtering}/introspection.py +2 -2
- oscura/utils/geometry.py +31 -0
- oscura/utils/imports.py +184 -0
- oscura/utils/lazy.py +1 -1
- oscura/{math → utils/math}/__init__.py +2 -2
- oscura/{math → utils/math}/arithmetic.py +114 -48
- oscura/{math → utils/math}/interpolation.py +139 -106
- oscura/utils/memory.py +129 -66
- oscura/utils/memory_advanced.py +92 -9
- oscura/utils/memory_extensions.py +10 -8
- oscura/{optimization → utils/optimization}/__init__.py +1 -1
- oscura/{optimization → utils/optimization}/search.py +2 -2
- oscura/utils/performance/__init__.py +58 -0
- oscura/utils/performance/caching.py +889 -0
- oscura/utils/performance/lsh_clustering.py +333 -0
- oscura/utils/performance/memory_optimizer.py +699 -0
- oscura/utils/performance/optimizations.py +675 -0
- oscura/utils/performance/parallel.py +654 -0
- oscura/utils/performance/profiling.py +661 -0
- oscura/{pipeline → utils/pipeline}/base.py +1 -1
- oscura/{pipeline → utils/pipeline}/composition.py +1 -1
- oscura/{pipeline → utils/pipeline}/parallel.py +3 -2
- oscura/{pipeline → utils/pipeline}/pipeline.py +1 -1
- oscura/{pipeline → utils/pipeline}/reverse_engineering.py +412 -221
- oscura/{search → utils/search}/__init__.py +3 -3
- oscura/{search → utils/search}/anomaly.py +188 -58
- oscura/utils/search/context.py +294 -0
- oscura/{search → utils/search}/pattern.py +138 -10
- oscura/utils/serial.py +51 -0
- oscura/utils/storage/__init__.py +61 -0
- oscura/utils/storage/database.py +1166 -0
- oscura/{streaming → utils/streaming}/chunked.py +302 -143
- oscura/{streaming → utils/streaming}/progressive.py +1 -1
- oscura/{streaming → utils/streaming}/realtime.py +3 -2
- oscura/{triggering → utils/triggering}/__init__.py +6 -6
- oscura/{triggering → utils/triggering}/base.py +6 -6
- oscura/{triggering → utils/triggering}/edge.py +2 -2
- oscura/{triggering → utils/triggering}/pattern.py +2 -2
- oscura/{triggering → utils/triggering}/pulse.py +115 -74
- oscura/{triggering → utils/triggering}/window.py +2 -2
- oscura/utils/validation.py +32 -0
- oscura/validation/__init__.py +121 -0
- oscura/{compliance → validation/compliance}/__init__.py +5 -5
- oscura/{compliance → validation/compliance}/advanced.py +5 -5
- oscura/{compliance → validation/compliance}/masks.py +1 -1
- oscura/{compliance → validation/compliance}/reporting.py +127 -53
- oscura/{compliance → validation/compliance}/testing.py +114 -52
- oscura/validation/compliance_tests.py +915 -0
- oscura/validation/fuzzer.py +990 -0
- oscura/validation/grammar_tests.py +596 -0
- oscura/validation/grammar_validator.py +904 -0
- oscura/validation/hil_testing.py +977 -0
- oscura/{quality → validation/quality}/__init__.py +4 -4
- oscura/{quality → validation/quality}/ensemble.py +251 -171
- oscura/{quality → validation/quality}/explainer.py +3 -3
- oscura/{quality → validation/quality}/scoring.py +1 -1
- oscura/{quality → validation/quality}/warnings.py +4 -4
- oscura/validation/regression_suite.py +808 -0
- oscura/validation/replay.py +788 -0
- oscura/{testing → validation/testing}/__init__.py +2 -2
- oscura/{testing → validation/testing}/synthetic.py +5 -5
- oscura/visualization/__init__.py +9 -0
- oscura/visualization/accessibility.py +1 -1
- oscura/visualization/annotations.py +64 -67
- oscura/visualization/colors.py +7 -7
- oscura/visualization/digital.py +180 -81
- oscura/visualization/eye.py +236 -85
- oscura/visualization/interactive.py +320 -143
- oscura/visualization/jitter.py +587 -247
- oscura/visualization/layout.py +169 -134
- oscura/visualization/optimization.py +103 -52
- oscura/visualization/palettes.py +1 -1
- oscura/visualization/power.py +427 -211
- oscura/visualization/power_extended.py +626 -297
- oscura/visualization/presets.py +2 -0
- oscura/visualization/protocols.py +495 -181
- oscura/visualization/render.py +79 -63
- oscura/visualization/reverse_engineering.py +171 -124
- oscura/visualization/signal_integrity.py +460 -279
- oscura/visualization/specialized.py +190 -100
- oscura/visualization/spectral.py +670 -255
- oscura/visualization/thumbnails.py +166 -137
- oscura/visualization/waveform.py +150 -63
- oscura/workflows/__init__.py +3 -0
- oscura/{batch → workflows/batch}/__init__.py +5 -5
- oscura/{batch → workflows/batch}/advanced.py +150 -75
- oscura/workflows/batch/aggregate.py +531 -0
- oscura/workflows/batch/analyze.py +236 -0
- oscura/{batch → workflows/batch}/logging.py +2 -2
- oscura/{batch → workflows/batch}/metrics.py +1 -1
- oscura/workflows/complete_re.py +1144 -0
- oscura/workflows/compliance.py +44 -54
- oscura/workflows/digital.py +197 -51
- oscura/workflows/legacy/__init__.py +12 -0
- oscura/{workflow → workflows/legacy}/dag.py +4 -1
- oscura/workflows/multi_trace.py +9 -9
- oscura/workflows/power.py +42 -62
- oscura/workflows/protocol.py +82 -49
- oscura/workflows/reverse_engineering.py +351 -150
- oscura/workflows/signal_integrity.py +157 -82
- oscura-0.7.0.dist-info/METADATA +661 -0
- oscura-0.7.0.dist-info/RECORD +591 -0
- oscura/batch/aggregate.py +0 -300
- oscura/batch/analyze.py +0 -139
- oscura/dsl/__init__.py +0 -73
- oscura/exceptions.py +0 -59
- oscura/exploratory/fuzzy.py +0 -513
- oscura/exploratory/sync.py +0 -384
- oscura/exporters/__init__.py +0 -94
- oscura/exporters/csv.py +0 -303
- oscura/exporters/exporters.py +0 -44
- oscura/exporters/hdf5.py +0 -217
- oscura/exporters/html_export.py +0 -701
- oscura/exporters/json_export.py +0 -291
- oscura/exporters/markdown_export.py +0 -367
- oscura/exporters/matlab_export.py +0 -354
- oscura/exporters/npz_export.py +0 -219
- oscura/exporters/spice_export.py +0 -210
- oscura/search/context.py +0 -149
- oscura/session/__init__.py +0 -34
- oscura/session/annotations.py +0 -289
- oscura/session/history.py +0 -313
- oscura/session/session.py +0 -520
- oscura/workflow/__init__.py +0 -13
- oscura-0.5.1.dist-info/METADATA +0 -583
- oscura-0.5.1.dist-info/RECORD +0 -481
- /oscura/core/{config.py → config/legacy.py} +0 -0
- /oscura/{extensibility → core/extensibility}/__init__.py +0 -0
- /oscura/{extensibility → core/extensibility}/registry.py +0 -0
- /oscura/{plugins → core/plugins}/isolation.py +0 -0
- /oscura/{schemas → core/schemas}/bus_configuration.json +0 -0
- /oscura/{builders → utils/builders}/signal_builder.py +0 -0
- /oscura/{optimization → utils/optimization}/parallel.py +0 -0
- /oscura/{pipeline → utils/pipeline}/__init__.py +0 -0
- /oscura/{streaming → utils/streaming}/__init__.py +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/WHEEL +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/entry_points.txt +0 -0
- {oscura-0.5.1.dist-info → oscura-0.7.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -172,6 +172,84 @@ class MessageFormatInferrer:
|
|
|
172
172
|
|
|
173
173
|
return schema
|
|
174
174
|
|
|
175
|
+
def _calculate_entropy_boundaries(
|
|
176
|
+
self, messages: list[NDArray[np.uint8]], msg_len: int
|
|
177
|
+
) -> list[int]:
|
|
178
|
+
"""Detect boundaries using entropy transitions.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
messages: List of message arrays
|
|
182
|
+
msg_len: Length of messages
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
List of boundary positions
|
|
186
|
+
"""
|
|
187
|
+
boundaries = []
|
|
188
|
+
entropies = []
|
|
189
|
+
|
|
190
|
+
# Calculate entropy at each byte position
|
|
191
|
+
for offset in range(msg_len):
|
|
192
|
+
entropy = self._calculate_byte_entropy(messages, offset)
|
|
193
|
+
entropies.append(entropy)
|
|
194
|
+
|
|
195
|
+
# Find transitions (entropy changes > threshold)
|
|
196
|
+
entropy_threshold = 1.5 # bits
|
|
197
|
+
for i in range(1, len(entropies)):
|
|
198
|
+
delta = abs(entropies[i] - entropies[i - 1])
|
|
199
|
+
if delta > entropy_threshold:
|
|
200
|
+
boundaries.append(i)
|
|
201
|
+
|
|
202
|
+
return boundaries
|
|
203
|
+
|
|
204
|
+
def _calculate_variance_boundaries(
|
|
205
|
+
self, messages: list[NDArray[np.uint8]], msg_len: int
|
|
206
|
+
) -> list[int]:
|
|
207
|
+
"""Detect boundaries using variance transitions.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
messages: List of message arrays
|
|
211
|
+
msg_len: Length of messages
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
List of boundary positions
|
|
215
|
+
"""
|
|
216
|
+
boundaries = []
|
|
217
|
+
variances = []
|
|
218
|
+
|
|
219
|
+
# Calculate variance at each byte position
|
|
220
|
+
for offset in range(msg_len):
|
|
221
|
+
values = [msg[offset] for msg in messages]
|
|
222
|
+
variance = np.var(values)
|
|
223
|
+
variances.append(variance)
|
|
224
|
+
|
|
225
|
+
# Find variance transitions
|
|
226
|
+
var_threshold = 1000.0
|
|
227
|
+
for i in range(1, len(variances)):
|
|
228
|
+
delta = abs(variances[i] - variances[i - 1])
|
|
229
|
+
if delta > var_threshold:
|
|
230
|
+
boundaries.append(i)
|
|
231
|
+
|
|
232
|
+
return boundaries
|
|
233
|
+
|
|
234
|
+
def _merge_close_boundaries(self, boundaries: list[int]) -> list[int]:
|
|
235
|
+
"""Merge boundaries that are too close together.
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
boundaries: Sorted list of boundaries
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Merged boundary list
|
|
242
|
+
"""
|
|
243
|
+
if not boundaries:
|
|
244
|
+
return []
|
|
245
|
+
|
|
246
|
+
merged = [boundaries[0]]
|
|
247
|
+
for b in boundaries[1:]:
|
|
248
|
+
if b - merged[-1] >= 2:
|
|
249
|
+
merged.append(b)
|
|
250
|
+
|
|
251
|
+
return merged
|
|
252
|
+
|
|
175
253
|
def detect_field_boundaries(
|
|
176
254
|
self,
|
|
177
255
|
messages: list[NDArray[np.uint8]],
|
|
@@ -194,45 +272,18 @@ class MessageFormatInferrer:
|
|
|
194
272
|
msg_len = len(messages[0])
|
|
195
273
|
boundaries = [0] # Always start at offset 0
|
|
196
274
|
|
|
275
|
+
# Detect boundaries using selected method
|
|
197
276
|
if method in ["entropy", "combined"]:
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
for offset in range(msg_len):
|
|
201
|
-
entropy = self._calculate_byte_entropy(messages, offset)
|
|
202
|
-
entropies.append(entropy)
|
|
203
|
-
|
|
204
|
-
# Find transitions (entropy changes > threshold)
|
|
205
|
-
entropy_threshold = 1.5 # bits
|
|
206
|
-
for i in range(1, len(entropies)):
|
|
207
|
-
delta = abs(entropies[i] - entropies[i - 1])
|
|
208
|
-
if delta > entropy_threshold and i not in boundaries:
|
|
209
|
-
boundaries.append(i)
|
|
277
|
+
entropy_boundaries = self._calculate_entropy_boundaries(messages, msg_len)
|
|
278
|
+
boundaries.extend(entropy_boundaries)
|
|
210
279
|
|
|
211
280
|
if method in ["variance", "combined"]:
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
for offset in range(msg_len):
|
|
215
|
-
values = [msg[offset] for msg in messages]
|
|
216
|
-
variance = np.var(values)
|
|
217
|
-
variances.append(variance)
|
|
218
|
-
|
|
219
|
-
# Find variance transitions
|
|
220
|
-
var_threshold = 1000.0
|
|
221
|
-
for i in range(1, len(variances)):
|
|
222
|
-
delta = abs(variances[i] - variances[i - 1])
|
|
223
|
-
if delta > var_threshold and i not in boundaries:
|
|
224
|
-
boundaries.append(i)
|
|
225
|
-
|
|
226
|
-
# Sort and ensure we don't have too many tiny fields
|
|
227
|
-
boundaries = sorted(set(boundaries))
|
|
281
|
+
variance_boundaries = self._calculate_variance_boundaries(messages, msg_len)
|
|
282
|
+
boundaries.extend(variance_boundaries)
|
|
228
283
|
|
|
229
|
-
# Merge
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if b - merged[-1] >= 2:
|
|
233
|
-
merged.append(b)
|
|
234
|
-
|
|
235
|
-
return merged
|
|
284
|
+
# Merge and sort boundaries
|
|
285
|
+
boundaries = sorted(set(boundaries))
|
|
286
|
+
return self._merge_close_boundaries(boundaries)
|
|
236
287
|
|
|
237
288
|
def detect_boundaries_voting(
|
|
238
289
|
self,
|
|
@@ -532,7 +583,30 @@ class MessageFormatInferrer:
|
|
|
532
583
|
if len(messages) < self.min_samples:
|
|
533
584
|
raise ValueError(f"Need at least {self.min_samples} messages, got {len(messages)}")
|
|
534
585
|
|
|
535
|
-
|
|
586
|
+
bytes_messages = self._convert_messages_to_bytes(messages)
|
|
587
|
+
msg_len = self._validate_message_lengths(bytes_messages)
|
|
588
|
+
boundaries = self.detect_boundaries_voting(
|
|
589
|
+
bytes_messages, min_confidence=min_boundary_confidence
|
|
590
|
+
)
|
|
591
|
+
msg_arrays = [np.frombuffer(msg, dtype=np.uint8) for msg in bytes_messages]
|
|
592
|
+
fields = self._infer_fields_from_boundaries(
|
|
593
|
+
boundaries, msg_arrays, msg_len, min_field_confidence
|
|
594
|
+
)
|
|
595
|
+
checksum_field, length_field = self._find_special_fields(fields)
|
|
596
|
+
header_size = self._estimate_header_size(fields)
|
|
597
|
+
|
|
598
|
+
return MessageSchema(
|
|
599
|
+
total_size=msg_len,
|
|
600
|
+
fields=fields,
|
|
601
|
+
field_boundaries=boundaries,
|
|
602
|
+
header_size=header_size,
|
|
603
|
+
payload_offset=header_size,
|
|
604
|
+
checksum_field=checksum_field,
|
|
605
|
+
length_field=length_field,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
def _convert_messages_to_bytes(self, messages: list[bytes | NDArray[np.uint8]]) -> list[bytes]:
|
|
609
|
+
"""Convert messages to bytes format."""
|
|
536
610
|
bytes_messages = []
|
|
537
611
|
for msg in messages:
|
|
538
612
|
if isinstance(msg, bytes):
|
|
@@ -541,99 +615,81 @@ class MessageFormatInferrer:
|
|
|
541
615
|
bytes_messages.append(msg.tobytes())
|
|
542
616
|
else:
|
|
543
617
|
raise ValueError(f"Invalid message type: {type(msg)}")
|
|
618
|
+
return bytes_messages
|
|
544
619
|
|
|
545
|
-
|
|
546
|
-
|
|
620
|
+
def _validate_message_lengths(self, messages: list[bytes]) -> int:
|
|
621
|
+
"""Validate all messages have same length and return that length."""
|
|
622
|
+
lengths = [len(m) for m in messages]
|
|
547
623
|
if len(set(lengths)) > 1:
|
|
548
624
|
raise ValueError(f"Messages have varying lengths: {set(lengths)}")
|
|
625
|
+
return lengths[0]
|
|
549
626
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
msg_arrays = []
|
|
559
|
-
for msg in bytes_messages:
|
|
560
|
-
msg_arrays.append(np.frombuffer(msg, dtype=np.uint8))
|
|
561
|
-
|
|
562
|
-
# Extract field candidates
|
|
627
|
+
def _infer_fields_from_boundaries(
|
|
628
|
+
self,
|
|
629
|
+
boundaries: list[int],
|
|
630
|
+
msg_arrays: list[NDArray[np.uint8]],
|
|
631
|
+
msg_len: int,
|
|
632
|
+
min_confidence: float,
|
|
633
|
+
) -> list[InferredField]:
|
|
634
|
+
"""Infer fields from detected boundaries."""
|
|
563
635
|
fields: list[InferredField] = []
|
|
564
636
|
for i in range(len(boundaries)):
|
|
565
637
|
offset = boundaries[i]
|
|
566
|
-
|
|
567
|
-
# Determine field size
|
|
568
|
-
if i < len(boundaries) - 1:
|
|
569
|
-
size = boundaries[i + 1] - offset
|
|
570
|
-
else:
|
|
571
|
-
size = msg_len - offset
|
|
572
|
-
|
|
573
|
-
# Extract field values
|
|
638
|
+
size = boundaries[i + 1] - offset if i < len(boundaries) - 1 else msg_len - offset
|
|
574
639
|
field_data = self._extract_field_data(msg_arrays, offset, size)
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
entropy_type, entropy_conf = self._detect_type_entropy(field_data)
|
|
578
|
-
pattern_type, pattern_conf = self._detect_type_patterns(
|
|
579
|
-
field_data, offset, size, msg_len
|
|
580
|
-
)
|
|
581
|
-
stats_type, stats_conf = self._detect_type_statistics(field_data)
|
|
582
|
-
|
|
583
|
-
# Vote on field type
|
|
584
|
-
field_type, confidence, evidence = self._vote_field_type(
|
|
585
|
-
[
|
|
586
|
-
(entropy_type, entropy_conf),
|
|
587
|
-
(pattern_type, pattern_conf),
|
|
588
|
-
(stats_type, stats_conf),
|
|
589
|
-
]
|
|
640
|
+
field_obj = self._classify_field_ensemble(
|
|
641
|
+
field_data, offset, size, msg_len, min_confidence
|
|
590
642
|
)
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
# Sample values (first 5)
|
|
594
|
-
sample_values = field_data["values"][:5]
|
|
595
|
-
|
|
596
|
-
field_obj = InferredField(
|
|
597
|
-
name=f"field_{len(fields)}",
|
|
598
|
-
offset=offset,
|
|
599
|
-
size=size,
|
|
600
|
-
field_type=field_type, # type: ignore[arg-type]
|
|
601
|
-
entropy=float(field_data["entropy"]),
|
|
602
|
-
variance=float(field_data["variance"]),
|
|
603
|
-
confidence=confidence,
|
|
604
|
-
values_seen=sample_values,
|
|
605
|
-
evidence=evidence,
|
|
606
|
-
)
|
|
607
|
-
|
|
643
|
+
if field_obj is not None:
|
|
644
|
+
field_obj.name = f"field_{len(fields)}"
|
|
608
645
|
fields.append(field_obj)
|
|
646
|
+
return fields
|
|
609
647
|
|
|
610
|
-
|
|
611
|
-
|
|
648
|
+
def _classify_field_ensemble(
|
|
649
|
+
self,
|
|
650
|
+
field_data: dict[str, Any],
|
|
651
|
+
offset: int,
|
|
652
|
+
size: int,
|
|
653
|
+
msg_len: int,
|
|
654
|
+
min_confidence: float,
|
|
655
|
+
) -> InferredField | None:
|
|
656
|
+
"""Classify field using ensemble of detectors."""
|
|
657
|
+
entropy_type, entropy_conf = self._detect_type_entropy(field_data)
|
|
658
|
+
pattern_type, pattern_conf = self._detect_type_patterns(field_data, offset, size, msg_len)
|
|
659
|
+
stats_type, stats_conf = self._detect_type_statistics(field_data)
|
|
660
|
+
field_type, confidence, evidence = self._vote_field_type(
|
|
661
|
+
[
|
|
662
|
+
(entropy_type, entropy_conf),
|
|
663
|
+
(pattern_type, pattern_conf),
|
|
664
|
+
(stats_type, stats_conf),
|
|
665
|
+
]
|
|
666
|
+
)
|
|
667
|
+
if confidence < min_confidence:
|
|
668
|
+
return None
|
|
669
|
+
return InferredField(
|
|
670
|
+
name="", # Will be set by caller
|
|
671
|
+
offset=offset,
|
|
672
|
+
size=size,
|
|
673
|
+
field_type=field_type, # type: ignore[arg-type]
|
|
674
|
+
entropy=float(field_data["entropy"]),
|
|
675
|
+
variance=float(field_data["variance"]),
|
|
676
|
+
confidence=confidence,
|
|
677
|
+
values_seen=field_data["values"][:5],
|
|
678
|
+
evidence=evidence,
|
|
679
|
+
)
|
|
612
680
|
|
|
613
|
-
|
|
681
|
+
def _find_special_fields(
|
|
682
|
+
self, fields: list[InferredField]
|
|
683
|
+
) -> tuple[InferredField | None, InferredField | None]:
|
|
684
|
+
"""Find checksum and length fields."""
|
|
614
685
|
checksum_field = None
|
|
615
686
|
length_field = None
|
|
616
|
-
|
|
617
687
|
for f in fields:
|
|
618
688
|
if f.field_type == "checksum":
|
|
619
689
|
checksum_field = f
|
|
620
690
|
elif f.field_type == "length":
|
|
621
691
|
length_field = f
|
|
622
|
-
|
|
623
|
-
# Payload starts after header
|
|
624
|
-
payload_offset = header_size
|
|
625
|
-
|
|
626
|
-
schema = MessageSchema(
|
|
627
|
-
total_size=msg_len,
|
|
628
|
-
fields=fields,
|
|
629
|
-
field_boundaries=boundaries,
|
|
630
|
-
header_size=header_size,
|
|
631
|
-
payload_offset=payload_offset,
|
|
632
|
-
checksum_field=checksum_field,
|
|
633
|
-
length_field=length_field,
|
|
634
|
-
)
|
|
635
|
-
|
|
636
|
-
return schema
|
|
692
|
+
return checksum_field, length_field
|
|
637
693
|
|
|
638
694
|
def _extract_field_data(
|
|
639
695
|
self, messages: list[NDArray[np.uint8]], offset: int, size: int
|
|
@@ -669,14 +725,14 @@ class MessageFormatInferrer:
|
|
|
669
725
|
int(msg[offset]) << 16 | int(msg[offset + 1]) << 8 | int(msg[offset + 2])
|
|
670
726
|
)
|
|
671
727
|
int_values.append(val_int)
|
|
672
|
-
values = list(int_values)
|
|
728
|
+
values = list(int_values)
|
|
673
729
|
else:
|
|
674
730
|
# For larger fields, use bytes
|
|
675
731
|
tuple_values: list[tuple[int, ...]] = []
|
|
676
732
|
for msg in messages:
|
|
677
733
|
val_tuple = tuple(int(b) for b in msg[offset : offset + size])
|
|
678
734
|
tuple_values.append(val_tuple)
|
|
679
|
-
values = list(tuple_values)
|
|
735
|
+
values = list(tuple_values)
|
|
680
736
|
|
|
681
737
|
# Calculate statistics
|
|
682
738
|
if size > 4:
|
|
@@ -733,57 +789,115 @@ class MessageFormatInferrer:
|
|
|
733
789
|
) -> tuple[str, float]:
|
|
734
790
|
"""Detect field type using pattern matching.
|
|
735
791
|
|
|
736
|
-
: Pattern-based field type detection.
|
|
737
|
-
|
|
738
792
|
Detects:
|
|
739
793
|
- Counters (incrementing values)
|
|
794
|
+
- Timestamps (steady large increments)
|
|
740
795
|
- Lengths (correlates with message size)
|
|
741
|
-
- Checksums (
|
|
796
|
+
- Checksums (end of message)
|
|
742
797
|
- Constants (no variation)
|
|
743
|
-
- Timestamps (steady increase)
|
|
744
798
|
|
|
745
799
|
Args:
|
|
746
|
-
field_data: Field data dictionary
|
|
747
|
-
offset: Field offset
|
|
748
|
-
size: Field size
|
|
749
|
-
msg_len: Total message length
|
|
800
|
+
field_data: Field data dictionary with 'values' key.
|
|
801
|
+
offset: Field offset in bytes.
|
|
802
|
+
size: Field size in bytes.
|
|
803
|
+
msg_len: Total message length in bytes.
|
|
750
804
|
|
|
751
805
|
Returns:
|
|
752
|
-
Tuple of (field_type,
|
|
806
|
+
Tuple of (field_type, confidence_score).
|
|
753
807
|
"""
|
|
754
808
|
values = field_data["values"]
|
|
755
809
|
|
|
756
|
-
#
|
|
757
|
-
if
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
# Check for checksum (near end of message, but not the entire message)
|
|
782
|
-
if offset + size >= msg_len - 4 and offset > 0:
|
|
810
|
+
# Skip tuple values (byte arrays)
|
|
811
|
+
if isinstance(values[0], tuple):
|
|
812
|
+
return ("unknown", 0.3)
|
|
813
|
+
|
|
814
|
+
# Extract integer values
|
|
815
|
+
int_values = [v for v in values if isinstance(v, int)]
|
|
816
|
+
if not int_values:
|
|
817
|
+
return ("unknown", 0.3)
|
|
818
|
+
|
|
819
|
+
# Check for counter pattern
|
|
820
|
+
if self._detect_counter_field(int_values):
|
|
821
|
+
return ("counter", 0.9)
|
|
822
|
+
|
|
823
|
+
# Check for timestamp pattern
|
|
824
|
+
timestamp_result = self._check_timestamp_pattern(int_values)
|
|
825
|
+
if timestamp_result is not None:
|
|
826
|
+
return timestamp_result
|
|
827
|
+
|
|
828
|
+
# Check for length field
|
|
829
|
+
if self._is_likely_length_field(offset, size, msg_len, int_values):
|
|
830
|
+
return ("length", 0.6)
|
|
831
|
+
|
|
832
|
+
# Check for checksum field
|
|
833
|
+
if self._is_likely_checksum_field(offset, size, msg_len):
|
|
783
834
|
return ("checksum", 0.5)
|
|
784
835
|
|
|
785
836
|
return ("unknown", 0.3)
|
|
786
837
|
|
|
838
|
+
def _check_timestamp_pattern(self, values: list[int]) -> tuple[str, float] | None:
|
|
839
|
+
"""Check if values follow timestamp pattern.
|
|
840
|
+
|
|
841
|
+
Args:
|
|
842
|
+
values: Integer values.
|
|
843
|
+
|
|
844
|
+
Returns:
|
|
845
|
+
('timestamp', confidence) or None if not a timestamp.
|
|
846
|
+
"""
|
|
847
|
+
if len(values) < 3:
|
|
848
|
+
return None
|
|
849
|
+
|
|
850
|
+
# Calculate differences between consecutive values
|
|
851
|
+
diffs = [values[i + 1] - values[i] for i in range(len(values) - 1)]
|
|
852
|
+
positive_diffs = [d for d in diffs if d > 0]
|
|
853
|
+
|
|
854
|
+
# Need mostly increasing values
|
|
855
|
+
if len(positive_diffs) < len(diffs) * 0.7:
|
|
856
|
+
return None
|
|
857
|
+
|
|
858
|
+
# Check for large increments (typical of timestamps)
|
|
859
|
+
avg_diff = sum(positive_diffs) / len(positive_diffs)
|
|
860
|
+
if avg_diff > 100:
|
|
861
|
+
return ("timestamp", 0.7)
|
|
862
|
+
|
|
863
|
+
return None
|
|
864
|
+
|
|
865
|
+
def _is_likely_length_field(
|
|
866
|
+
self, offset: int, size: int, msg_len: int, values: list[int]
|
|
867
|
+
) -> bool:
|
|
868
|
+
"""Check if field is likely a length field.
|
|
869
|
+
|
|
870
|
+
Args:
|
|
871
|
+
offset: Field offset.
|
|
872
|
+
size: Field size.
|
|
873
|
+
msg_len: Message length.
|
|
874
|
+
values: Integer values.
|
|
875
|
+
|
|
876
|
+
Returns:
|
|
877
|
+
True if likely a length field.
|
|
878
|
+
"""
|
|
879
|
+
# Length fields are typically near message start and small
|
|
880
|
+
if offset >= 8 or size > 2:
|
|
881
|
+
return False
|
|
882
|
+
|
|
883
|
+
# Values should be reasonable for message lengths
|
|
884
|
+
max_val = max(values)
|
|
885
|
+
return max_val < msg_len * 2
|
|
886
|
+
|
|
887
|
+
def _is_likely_checksum_field(self, offset: int, size: int, msg_len: int) -> bool:
|
|
888
|
+
"""Check if field is likely a checksum.
|
|
889
|
+
|
|
890
|
+
Args:
|
|
891
|
+
offset: Field offset.
|
|
892
|
+
size: Field size.
|
|
893
|
+
msg_len: Message length.
|
|
894
|
+
|
|
895
|
+
Returns:
|
|
896
|
+
True if likely a checksum field.
|
|
897
|
+
"""
|
|
898
|
+
# Checksums are typically near the end but not the whole message
|
|
899
|
+
return offset + size >= msg_len - 4 and offset > 0
|
|
900
|
+
|
|
787
901
|
def _detect_type_statistics(self, field_data: dict[str, Any]) -> tuple[str, float]:
|
|
788
902
|
"""Detect field type using statistical properties.
|
|
789
903
|
|
|
@@ -909,14 +1023,14 @@ class MessageFormatInferrer:
|
|
|
909
1023
|
| int(msg[offset + 2])
|
|
910
1024
|
)
|
|
911
1025
|
int_values.append(val_int)
|
|
912
|
-
values = list(int_values)
|
|
1026
|
+
values = list(int_values)
|
|
913
1027
|
else:
|
|
914
1028
|
# For larger fields, use bytes
|
|
915
1029
|
tuple_values: list[tuple[int, ...]] = []
|
|
916
1030
|
for msg in messages:
|
|
917
1031
|
val_tuple = tuple(int(b) for b in msg[offset : offset + size])
|
|
918
1032
|
tuple_values.append(val_tuple)
|
|
919
|
-
values = list(tuple_values)
|
|
1033
|
+
values = list(tuple_values)
|
|
920
1034
|
|
|
921
1035
|
# Calculate statistics
|
|
922
1036
|
if size > 4:
|
|
@@ -1025,55 +1139,100 @@ class MessageFormatInferrer:
|
|
|
1025
1139
|
) -> tuple[str, float]:
|
|
1026
1140
|
"""Classify field type based on patterns.
|
|
1027
1141
|
|
|
1028
|
-
: Field type classification logic.
|
|
1029
|
-
|
|
1030
1142
|
Args:
|
|
1031
|
-
values: Field values across all messages
|
|
1032
|
-
offset: Field offset
|
|
1033
|
-
size: Field size
|
|
1034
|
-
messages: Original
|
|
1143
|
+
values: Field values across all messages.
|
|
1144
|
+
offset: Field offset in bytes.
|
|
1145
|
+
size: Field size in bytes.
|
|
1146
|
+
messages: Original message byte arrays.
|
|
1035
1147
|
|
|
1036
1148
|
Returns:
|
|
1037
|
-
Tuple of (field_type,
|
|
1149
|
+
Tuple of (field_type, confidence_score).
|
|
1150
|
+
|
|
1151
|
+
Example:
|
|
1152
|
+
>>> field_type, confidence = analyzer._classify_field(values, 0, 1, messages)
|
|
1038
1153
|
"""
|
|
1039
|
-
# Handle byte fields (
|
|
1154
|
+
# Handle byte array fields (tuples)
|
|
1040
1155
|
if isinstance(values[0], tuple):
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1156
|
+
return self._classify_byte_array_field(values)
|
|
1157
|
+
|
|
1158
|
+
# Handle scalar fields
|
|
1159
|
+
return self._classify_scalar_field(values, offset, size, messages)
|
|
1160
|
+
|
|
1161
|
+
def _classify_byte_array_field(self, values: list[int | tuple[int, ...]]) -> tuple[str, float]:
|
|
1162
|
+
"""Classify byte array field (multi-byte fields).
|
|
1163
|
+
|
|
1164
|
+
Args:
|
|
1165
|
+
values: List of byte tuples.
|
|
1166
|
+
|
|
1167
|
+
Returns:
|
|
1168
|
+
Tuple of (field_type, confidence).
|
|
1169
|
+
"""
|
|
1170
|
+
# Check for constant byte arrays
|
|
1171
|
+
if len(set(values)) == 1:
|
|
1172
|
+
return ("constant", 1.0)
|
|
1173
|
+
|
|
1174
|
+
# Calculate entropy across all bytes
|
|
1175
|
+
all_bytes = np.concatenate([np.array(v) for v in values])
|
|
1176
|
+
entropy = self._calculate_entropy(all_bytes)
|
|
1177
|
+
|
|
1178
|
+
if entropy < 1.0:
|
|
1179
|
+
return ("constant", 0.9)
|
|
1180
|
+
elif entropy > 7.0:
|
|
1181
|
+
return ("data", 0.6)
|
|
1182
|
+
else:
|
|
1183
|
+
return ("data", 0.5)
|
|
1184
|
+
|
|
1185
|
+
def _classify_scalar_field(
|
|
1186
|
+
self,
|
|
1187
|
+
values: list[int | tuple[int, ...]],
|
|
1188
|
+
offset: int,
|
|
1189
|
+
size: int,
|
|
1190
|
+
messages: list[NDArray[np.uint8]],
|
|
1191
|
+
) -> tuple[str, float]:
|
|
1192
|
+
"""Classify scalar (integer) field.
|
|
1052
1193
|
|
|
1053
|
-
|
|
1194
|
+
Args:
|
|
1195
|
+
values: List of integer values.
|
|
1196
|
+
offset: Field offset.
|
|
1197
|
+
size: Field size.
|
|
1198
|
+
messages: Original messages.
|
|
1199
|
+
|
|
1200
|
+
Returns:
|
|
1201
|
+
Tuple of (field_type, confidence).
|
|
1202
|
+
"""
|
|
1203
|
+
# Check for constant values
|
|
1054
1204
|
if len(set(values)) == 1:
|
|
1055
1205
|
return ("constant", 1.0)
|
|
1056
1206
|
|
|
1057
|
-
# Check for counter
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
):
|
|
1207
|
+
# Check for counter pattern
|
|
1208
|
+
int_values = [v for v in values if isinstance(v, int)]
|
|
1209
|
+
if self._detect_counter_field(int_values):
|
|
1061
1210
|
return ("counter", 0.9)
|
|
1062
1211
|
|
|
1063
|
-
# Check for checksum (
|
|
1212
|
+
# Check for checksum (near end of message)
|
|
1064
1213
|
msg_len = len(messages[0])
|
|
1065
|
-
if offset + size >= msg_len - 4:
|
|
1214
|
+
if offset + size >= msg_len - 4:
|
|
1066
1215
|
if self._detect_checksum_field(messages, offset, size):
|
|
1067
1216
|
return ("checksum", 0.8)
|
|
1068
1217
|
|
|
1069
|
-
# Check for length field (small values
|
|
1218
|
+
# Check for length field (early, small values)
|
|
1070
1219
|
if offset < 8 and size <= 2:
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
return ("length", 0.6)
|
|
1220
|
+
max_val = max(int_values)
|
|
1221
|
+
if max_val < msg_len * 2:
|
|
1222
|
+
return ("length", 0.6)
|
|
1075
1223
|
|
|
1076
|
-
#
|
|
1224
|
+
# Classify by variance and entropy
|
|
1225
|
+
return self._classify_by_statistics(values)
|
|
1226
|
+
|
|
1227
|
+
def _classify_by_statistics(self, values: list[int | tuple[int, ...]]) -> tuple[str, float]:
|
|
1228
|
+
"""Classify field by statistical properties.
|
|
1229
|
+
|
|
1230
|
+
Args:
|
|
1231
|
+
values: Field values.
|
|
1232
|
+
|
|
1233
|
+
Returns:
|
|
1234
|
+
Tuple of (field_type, confidence).
|
|
1235
|
+
"""
|
|
1077
1236
|
variance = np.var(values)
|
|
1078
1237
|
entropy = self._calculate_entropy(np.array(values))
|
|
1079
1238
|
|
|
@@ -1304,4 +1463,4 @@ def find_dependencies(
|
|
|
1304
1463
|
# Cast to expected type (msg_arrays contains only NDArray after conversion)
|
|
1305
1464
|
schema = inferrer.infer_format(msg_arrays) # type: ignore[arg-type]
|
|
1306
1465
|
|
|
1307
|
-
return inferrer.find_dependencies(msg_arrays, schema)
|
|
1466
|
+
return inferrer.find_dependencies(msg_arrays, schema)
|