pwn 0.5.729 → 0.5.731

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '04697e334aa79768d74f64b19c7e90c8a8b0372899abdf68d83bcfd4f2dcf96b'
4
- data.tar.gz: 8e8934c327d039dbfaad46d62837d1e6241af718b915deba0ed3aac240ded64b
3
+ metadata.gz: fba7b3d4693ca9b846825957a7bde24e2a1cd6fd18577666cac32f3abfc0fcb2
4
+ data.tar.gz: a1ea687f125fb659f363d7195ac4f32f332918b7707ecf1aa78acafa653eff36
5
5
  SHA512:
6
- metadata.gz: 68965dcb416fa10c340a8aaa4345409313f8949de2ea17529fbe570b35022e0eaa782639941f5d1ed873542dc6d1432a20372c0c16c1365ff32c199635829921
7
- data.tar.gz: cb5176d386d0d26308ad52ac292714ddb11134fe384cf994ce275326a6d6dffbfa628f5616af99b7f6d44c9cf7df0add18a4f4d9669805e85278851da1b32a17
6
+ metadata.gz: 50cd5f5f05075760b364ef46dc82ff469d1698bbaf724880563f97793108437fcfbbc441e2283817ce10b44c46f36566b6254f36879be66f67432cdeecc8c57a
7
+ data.tar.gz: 2085d58ae27b634c5d0893ab0c30f428e9442a15695721a4e45d3c923cbbaa67eb95d35e578bb07d1e1b6253be089ac2c833381ba839adf406c2137229063cd3
data/Gemfile CHANGED
@@ -50,9 +50,9 @@ gem 'libusb', '0.8.0'
50
50
  gem 'luhn', '3.0.0'
51
51
  gem 'mail', '2.9.1'
52
52
  gem 'mcp', '1.5.1'
53
- gem 'meshtastic', '0.0.175'
53
+ gem 'meshtastic', '0.0.176'
54
54
  gem 'metasm', '1.0.6'
55
- gem 'mongo', '2.25.0'
55
+ gem 'mongo', '2.26.0'
56
56
  gem 'msfrpc-client', '1.1.2'
57
57
  gem 'netaddr', '2.0.6'
58
58
  gem 'net-ldap', '0.20.0'
@@ -81,7 +81,7 @@ gem 'rmagick', '7.1.4'
81
81
  gem 'rqrcode', '3.2.0'
82
82
  gem 'rspec', '3.13.2'
83
83
  gem 'rtesseract', '3.1.4'
84
- gem 'rubocop', '1.90.0'
84
+ gem 'rubocop', '1.91.0'
85
85
  gem 'rubocop-rake', '0.7.1'
86
86
  gem 'rubocop-rspec', '3.10.2'
87
87
  gem 'ruby-audio', '1.6.1'
@@ -60,6 +60,67 @@ An already-true `stop:` callback raises cancellation before spawning either clie
60
60
  Invalid inputs still raise `ArgumentError`; native decoding never silently falls
61
61
  back to another decoder or a detector.
62
62
 
63
+ ### Optional Liquid and radare2 integration tests
64
+
65
+ Native Liquid DSP examples and the real radare2 JSON replay are also explicit
66
+ opt-ins, even when the backends happen to be installed. The default gate excludes
67
+ `liquid_integration` and `radare2_integration` metadata rather than marking tests
68
+ pending. Liquid interface checks, missing-library errors, pure-Ruby DSP fallback,
69
+ radare2 argument validation, and binutils fallback coverage still run by default.
70
+ The binary fallback tests require the existing binutils tools and `/bin/true`.
71
+
72
+ ```bash
73
+ PWN_TEST_LIQUID=1 bundle exec rspec spec/lib/pwn/ffi/liquid_spec.rb
74
+ PWN_TEST_RADARE2=1 bundle exec rspec spec/lib/pwn/plugins/binary_analysis_spec.rb
75
+ # Enable both in the full gate:
76
+ PWN_TEST_LIQUID=1 PWN_TEST_RADARE2=1 bundle exec rake
77
+ ```
78
+
79
+ Liquid opt-in requires the liquid-dsp shared library (`libliquid`) visible to the
80
+ dynamic loader. Radare2 opt-in requires `r2` and a C compiler named `cc` on PATH.
81
+ The radare2 example compiles a small unstripped C fixture in a temporary directory,
82
+ checks real JSON disassembly for its known `main` symbol, and removes the fixture
83
+ afterward. It does not assume the host's stripped `/bin/true` has a discoverable
84
+ `main`. Opt-in runs fail with installation guidance when a prerequisite is absent;
85
+ they never skip or accept a degraded backend as native success. The tests install
86
+ nothing and require no radio hardware or network access.
87
+
88
+ ### Optional FFTW, Volk, and SDR shared-library tests
89
+
90
+ The default gate also excludes real FFTW/Volk computations and radio-library
91
+ integration checks, regardless of what is installed. Missing-library behavior,
92
+ mocked device inventory and stream lifecycle tests, and Ruby DSP fallbacks run
93
+ without these shared libraries and produce no pending examples.
94
+
95
+ | Environment opt-in | RSpec metadata | Loader prerequisite / coverage |
96
+ | --- | --- | --- |
97
+ | `PWN_TEST_FFTW=1` | `fftw_integration` | `libfftw3f`; native impulse FFT |
98
+ | `PWN_TEST_VOLK=1` | `volk_integration` | `libvolk`; native conversion, accumulation, DSP dispatch |
99
+ | `PWN_TEST_RTL_SDR=1` | `rtl_sdr_integration` | `librtlsdr`; binding resolution only, no USB scan |
100
+ | `PWN_TEST_ADALM_PLUTO=1` | `adalm_pluto_integration` | Compatible `libiio`; library version only |
101
+ | `PWN_TEST_SOAPY_SDR=1` | `soapy_sdr_integration` | `libSoapySDR`; library/API version only |
102
+ | `PWN_TEST_HACK_RF=1` | `hack_rf_integration` | `libhackrf`; library version only |
103
+
104
+ For example:
105
+
106
+ ```bash
107
+ PWN_TEST_FFTW=1 bundle exec rspec spec/lib/pwn/ffi/fftw_spec.rb
108
+ PWN_TEST_VOLK=1 bundle exec rspec spec/lib/pwn/ffi/volk_spec.rb spec/lib/pwn/sdr/decoder/dsp_spec.rb
109
+ PWN_TEST_ADALM_PLUTO=1 bundle exec rspec spec/lib/pwn/ffi/adalm_pluto_spec.rb
110
+ PWN_TEST_LIQUID=1 bundle exec rspec spec/lib/pwn/ffi/liquid_spec.rb spec/lib/pwn/sdr/decoder/dsp_spec.rb
111
+ ```
112
+
113
+ Each opt-in fails with installation guidance if its library cannot load. Native
114
+ DSP integration tests assert real backend dispatch, not just a matching fallback.
115
+ None of these library opt-ins opens, tunes, receives from, or transmits through a
116
+ radio. Pluto URI discovery is mocked, including scan cleanup.
117
+
118
+ Actual RTL-SDR USB enumeration has a separate `rtl_sdr_hardware` tag, enabled
119
+ only by `PWN_TEST_RTL_SDR_HARDWARE=1`. It requires a connected RTL-SDR and USB
120
+ permissions, and fails on an empty inventory. Do not enable it in unattended
121
+ upgrade gates; library opt-ins do not enable hardware tests. It performs inventory
122
+ only, not tuning or reception. The test suite installs no dependencies.
123
+
63
124
  ## Adding a plugin
64
125
 
65
126
  1. `lib/pwn/plugins/my_thing.rb` following the conventions above.
data/lib/pwn/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module PWN
4
- VERSION = '0.5.729'
4
+ VERSION = '0.5.731'
5
5
  end
@@ -101,15 +101,57 @@ RSpec.describe 'PWN::AI::Agent::Verification' do
101
101
  expect(report[:status]).to eq(:fail)
102
102
  end
103
103
 
104
- it 'executes opt-in test commands with bounded duration and treats timeouts as unknown' do
105
- opts = { request: 'Run tests', requirements: ['Run tests'], root: @root, allow_commands: true, timeout: 0.2 }
106
- check = { requirement: 'Run tests', kind: :command, argv: [RbConfig.ruby, '-e', 'puts "verified"'], expected: "verified\n" }
107
- expect(PWN::AI::Agent::Verification.run(opts.merge(checks: [check]))[:status]).to eq(:pass)
108
- check[:argv] = [RbConfig.ruby, '-e', 'exit 1']
109
- expect(PWN::AI::Agent::Verification.run(opts.merge(checks: [check]))[:status]).to eq(:fail)
110
- check[:argv] = [RbConfig.ruby, '-e', 'sleep 10']
111
- expect(PWN::AI::Agent::Verification.run(opts.merge(checks: [check]))[:status]).to eq(:unknown)
112
- expect(PWN::AI::Agent::Verification.run(opts.merge(allow_commands: false, checks: [check]))[:status]).to eq(:unknown)
104
+ it 'executes opt-in test commands with a bounded budget that allows interpreter startup' do
105
+ report = PWN::AI::Agent::Verification.run(
106
+ request: 'Run tests', requirements: ['Run tests'], root: @root, allow_commands: true, timeout: 10,
107
+ checks: [{ requirement: 'Run tests', kind: :command, argv: [RbConfig.ruby, '-e', 'puts "verified"'], expected: "verified\n" }]
108
+ )
109
+ expect(report[:status]).to eq(:pass)
110
+ expect(report[:checks].first).to include(passed: true, exit_code: 0, evidence: Digest::SHA256.hexdigest("verified\n"))
111
+ end
112
+
113
+ it 'allows simulated slow interpreter startup within the successful command budget' do
114
+ # Loading a startup file before -e models a slower host without mocking the runner.
115
+ startup = File.join(@root, 'startup.rb')
116
+ File.write(startup, 'sleep 0.3')
117
+ report = PWN::AI::Agent::Verification.run(
118
+ request: 'Run tests', requirements: ['Run tests'], root: @root, allow_commands: true, timeout: 10,
119
+ checks: [{ requirement: 'Run tests', kind: :command, argv: [RbConfig.ruby, '-r', startup, '-e', 'puts "verified"'], expected: "verified\n" }]
120
+ )
121
+ expect(report[:status]).to eq(:pass)
122
+ expect(report[:checks].first).to include(passed: true, exit_code: 0, evidence: Digest::SHA256.hexdigest("verified\n"))
123
+ end
124
+
125
+ it 'reports an actual unsuccessful command exit as failure' do
126
+ report = PWN::AI::Agent::Verification.run(
127
+ request: 'Run tests', requirements: ['Run tests'], root: @root, allow_commands: true, timeout: 10,
128
+ checks: [{ requirement: 'Run tests', kind: :command, argv: [RbConfig.ruby, '-e', 'exit 1'] }]
129
+ )
130
+ expect(report[:status]).to eq(:fail)
131
+ expect(report[:checks].first).to include(passed: false, exit_code: 1)
132
+ end
133
+
134
+ it 'bounds an unfinished command and reports a timeout as unknown, not failure or success' do
135
+ # This command cannot finish within the deadline, regardless of startup speed.
136
+ report = Timeout.timeout(5) do
137
+ PWN::AI::Agent::Verification.run(
138
+ request: 'Run tests', requirements: ['Run tests'], root: @root, allow_commands: true, timeout: 0.2,
139
+ checks: [{ requirement: 'Run tests', kind: :command, argv: [RbConfig.ruby, '-e', 'sleep 60'] }]
140
+ )
141
+ end
142
+ expect(report[:status]).to eq(:unknown)
143
+ expect(report[:checks].first).to include(passed: nil, evidence: 'unavailable', error: 'Timeout::Error')
144
+ expect(report[:checks].first).not_to have_key(:exit_code)
145
+ end
146
+
147
+ it 'does not spawn test commands without explicit opt-in' do
148
+ expect(Open3).not_to receive(:popen3)
149
+ report = PWN::AI::Agent::Verification.run(
150
+ request: 'Run tests', requirements: ['Run tests'], root: @root, allow_commands: false,
151
+ checks: [{ requirement: 'Run tests', kind: :command, argv: [RbConfig.ruby, '-e', 'puts "verified"'] }]
152
+ )
153
+ expect(report[:status]).to eq(:unknown)
154
+ expect(report[:checks].first).to include(passed: nil, error: 'ArgumentError')
113
155
  end
114
156
 
115
157
  it 'attributes a checked artifact only to its last observed writer rather than a later noop' do
@@ -3,6 +3,12 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe PWN::FFI::AdalmPluto do
6
+ it 'handles a missing shared library without native calls' do
7
+ allow(described_class).to receive(:available?).and_return(false)
8
+ expect(described_class.info).to include(available: false)
9
+ expect(described_class.list_uris).to eq([])
10
+ end
11
+
6
12
  it 'waits for a cancelled refill to return before destroying its buffer' do
7
13
  handle = { buffer: FFI::MemoryPointer.new(:char, 4) }
8
14
  entered = Queue.new
@@ -62,8 +68,8 @@ describe PWN::FFI::AdalmPluto do
62
68
  expect(PWN::FFI::AdalmPluto).to respond_to :available?
63
69
  end
64
70
 
65
- it 'should report library info when libiio is present' do
66
- skip 'libiio not installed' unless PWN::FFI::AdalmPluto.available?
71
+ it 'should report library info', :adalm_pluto_integration do
72
+ expect(described_class.available?).to be(true), 'Install compatible libiio and make it visible to the dynamic loader (PWN_TEST_ADALM_PLUTO=1).'
67
73
 
68
74
  info = PWN::FFI::AdalmPluto.info
69
75
  expect(info[:available]).to eq(true)
@@ -71,15 +77,23 @@ describe PWN::FFI::AdalmPluto do
71
77
  expect(info[:minor]).to be_a(Integer)
72
78
  end
73
79
 
74
- it 'should list URIs (possibly empty) when libiio is present' do
75
- skip 'libiio not installed' unless PWN::FFI::AdalmPluto.available?
76
-
77
- # Restrict to usb,local so libiio does not attempt mDNS/DNS-SD (avahi)
78
- # discovery during the test suite — avoids the noisy
79
- # "ERROR: Unable to create Avahi DNS-SD client :Daemon not running"
80
- # C-level stderr write on hosts where avahi-daemon is not running.
81
- list = PWN::FFI::AdalmPluto.list_uris(backends: 'usb,local')
82
- expect(list).to be_a(Array)
80
+ it 'maps URIs and releases the mocked scan without hardware discovery' do
81
+ allow(described_class).to receive(:available?).and_return(true)
82
+ scan = FFI::MemoryPointer.new(:char)
83
+ entry = FFI::MemoryPointer.new(:char)
84
+ entries = FFI::MemoryPointer.new(:pointer)
85
+ entries.write_pointer(entry)
86
+ expect(described_class).to receive(:iio_create_scan_context).with('usb,local', 0).and_return(scan)
87
+ expect(described_class).to receive(:iio_scan_context_get_info_list) do |context, output|
88
+ expect(context).to eq(scan)
89
+ output.write_pointer(entries)
90
+ 1
91
+ end
92
+ allow(described_class).to receive(:iio_context_info_get_uri).with(entry).and_return('usb:mock')
93
+ allow(described_class).to receive(:iio_context_info_get_description).with(entry).and_return('Mock Pluto')
94
+ expect(described_class).to receive(:iio_context_info_list_free).with(entries)
95
+ expect(described_class).to receive(:iio_scan_context_destroy).with(scan)
96
+ expect(described_class.list_uris(backends: 'usb,local')).to eq([{ uri: 'usb:mock', description: 'Mock Pluto' }])
83
97
  end
84
98
 
85
99
  it 'should appear in PWN::FFI.backends' do
@@ -3,6 +3,11 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe PWN::FFI::FFTW do
6
+ it 'handles a missing shared library without native calls' do
7
+ allow(described_class).to receive(:available?).and_return(false)
8
+ expect { described_class.rfft(samples: [1.0]) }.to raise_error(RuntimeError, /libfftw3f not available/)
9
+ end
10
+
6
11
  it 'should display information for authors' do
7
12
  expect(PWN::FFI::FFTW).to respond_to :authors
8
13
  end
@@ -15,8 +20,8 @@ describe PWN::FFI::FFTW do
15
20
  expect(PWN::FFI::FFTW).to respond_to :available?
16
21
  end
17
22
 
18
- it 'should compute an rfft impulse response when libfftw3f is present' do
19
- skip 'libfftw3f not installed' unless PWN::FFI::FFTW.available?
23
+ it 'should compute an rfft impulse response', :fftw_integration do
24
+ expect(described_class.available?).to be(true), 'Install libfftw3f and make it visible to the dynamic loader (PWN_TEST_FFTW=1).'
20
25
 
21
26
  spec = PWN::FFI::FFTW.rfft(samples: [1.0, 0, 0, 0, 0, 0, 0, 0])
22
27
  expect(spec.length).to eq(5) # n/2+1
@@ -3,6 +3,12 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe PWN::FFI::HackRF do
6
+ it 'handles a missing shared library without native calls' do
7
+ allow(described_class).to receive(:available?).and_return(false)
8
+ expect(described_class.info).to include(available: false)
9
+ expect { described_class.open }.to raise_error(RuntimeError, /libhackrf not available/)
10
+ end
11
+
6
12
  it 'releases the GVL while native stop waits for callbacks' do
7
13
  source = File.read(File.expand_path('../../../../lib/pwn/ffi/hack_rf.rb', __dir__))
8
14
  expect(source).to match(/attach_function :hackrf_stop_rx, \[:pointer\], :int, blocking: true/)
@@ -51,8 +57,8 @@ describe PWN::FFI::HackRF do
51
57
  expect(PWN::FFI::HackRF).to respond_to :available?
52
58
  end
53
59
 
54
- it 'should report library info when libhackrf is present' do
55
- skip 'libhackrf not installed' unless PWN::FFI::HackRF.available?
60
+ it 'should report library info', :hack_rf_integration do
61
+ expect(described_class.available?).to be(true), 'Install libhackrf and make it visible to the dynamic loader (PWN_TEST_HACK_RF=1).'
56
62
 
57
63
  info = PWN::FFI::HackRF.info
58
64
  expect(info[:available]).to eq(true)
@@ -15,17 +15,33 @@ describe PWN::FFI::Liquid do
15
15
  expect(PWN::FFI::Liquid).to respond_to :available?
16
16
  end
17
17
 
18
- it 'should resample at half rate when libliquid is present' do
19
- skip 'libliquid not installed' unless PWN::FFI::Liquid.available?
18
+ it 'rejects native DSP calls when libliquid is unavailable' do
19
+ allow(described_class).to receive(:available?).and_return(false)
20
+ expect { described_class.resample(samples: [0.0], rate: 0.5) }.to raise_error(RuntimeError, /libliquid not available/)
21
+ expect { described_class.freq_demod(iq: [1.0, 0.0]) }.to raise_error(RuntimeError, /libliquid not available/)
22
+ end
23
+
24
+ it 'keeps DSP resampling functional when libliquid is unavailable' do
25
+ allow(described_class).to receive(:available?).and_return(false)
26
+ allow(PWN::SDR::Decoder::DSP).to receive(:native).and_return(true)
27
+ expect(described_class).not_to receive(:resample)
28
+ result = PWN::SDR::Decoder::DSP.resample(samples: [0.0, 1.0, 2.0, 3.0], src_rate: 4, dst_rate: 2)
29
+ expect(result).to eq([0.0, 2.0])
30
+ end
20
31
 
32
+ before(:each, :liquid_integration) do
33
+ expect(described_class.available?).to be(true),
34
+ 'PWN_TEST_LIQUID=1 requires liquid-dsp: install libliquid and make it visible to the dynamic loader. ' \
35
+ "Load error: #{described_class.load_error}"
36
+ end
37
+
38
+ it 'should resample at half rate with libliquid', :liquid_integration do
21
39
  samples = Array.new(64) { |i| Math.sin(2 * Math::PI * i / 16.0) }
22
40
  out = PWN::FFI::Liquid.resample(samples: samples, rate: 0.5)
23
41
  expect(out.length).to be_between(24, 40)
24
42
  end
25
43
 
26
- it 'should FM-demod a complex tone when libliquid is present' do
27
- skip 'libliquid not installed' unless PWN::FFI::Liquid.available?
28
-
44
+ it 'should FM-demod a complex tone with libliquid', :liquid_integration do
29
45
  iq = []
30
46
  phase = 0.0
31
47
  dphi = 2 * Math::PI * 0.05
@@ -3,6 +3,12 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe PWN::FFI::RTLSdr do
6
+ it 'handles a missing shared library without native calls' do
7
+ allow(described_class).to receive(:available?).and_return(false)
8
+ expect { described_class.list_devices }.to raise_error(RuntimeError, /librtlsdr not available/)
9
+ expect { described_class.open }.to raise_error(RuntimeError, /librtlsdr not available/)
10
+ end
11
+
6
12
  it 'releases the GVL during native reads so DSP workers can run' do
7
13
  source = File.read(File.expand_path('../../../../lib/pwn/ffi/rtl_sdr.rb', __dir__))
8
14
  declaration = source[/attach_function :rtlsdr_read_sync,.*?(?=attach_function)/m]
@@ -31,10 +37,27 @@ describe PWN::FFI::RTLSdr do
31
37
  expect(PWN::FFI::RTLSdr).to respond_to :available?
32
38
  end
33
39
 
34
- it 'should list devices (possibly empty) when librtlsdr is present' do
35
- skip 'librtlsdr not installed' unless PWN::FFI::RTLSdr.available?
40
+ it 'maps device inventory using mocked USB calls' do
41
+ allow(described_class).to receive(:available?).and_return(true)
42
+ allow(described_class).to receive(:rtlsdr_get_device_count).and_return(1)
43
+ allow(described_class).to receive(:rtlsdr_get_device_name).with(0).and_return('Mock tuner')
44
+ expect(described_class).to receive(:rtlsdr_get_device_usb_strings) do |index, manufacturer, product, serial|
45
+ expect(index).to eq(0)
46
+ manufacturer.write_string('Test')
47
+ product.write_string('SDR')
48
+ serial.write_string('123')
49
+ 0
50
+ end
51
+ expect(described_class.list_devices).to eq([{ index: 0, name: 'Mock tuner', manufacturer: 'Test', product: 'SDR', serial: '123' }])
52
+ end
53
+
54
+ it 'loads the native inventory and read bindings without scanning USB', :rtl_sdr_integration do
55
+ expect(described_class.available?).to be(true), 'Install librtlsdr and make it visible to the dynamic loader (PWN_TEST_RTL_SDR=1).'
56
+ expect(described_class).to respond_to(:rtlsdr_get_device_count, :rtlsdr_read_sync)
57
+ end
36
58
 
37
- list = PWN::FFI::RTLSdr.list_devices
38
- expect(list).to be_a(Array)
59
+ it 'lists a connected RTL-SDR', :rtl_sdr_hardware do
60
+ expect(described_class.available?).to be(true), 'Install librtlsdr before enabling PWN_TEST_RTL_SDR_HARDWARE=1.'
61
+ expect(described_class.list_devices).not_to be_empty, 'Connect an RTL-SDR with USB permissions before enabling PWN_TEST_RTL_SDR_HARDWARE=1.'
39
62
  end
40
63
  end
@@ -3,6 +3,12 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe PWN::FFI::SoapySDR do
6
+ it 'handles a missing shared library without native calls' do
7
+ allow(described_class).to receive(:available?).and_return(false)
8
+ expect(described_class.info).to include(available: false)
9
+ expect { described_class.make(args: 'driver=mock') }.to raise_error(RuntimeError, /libSoapySDR not available/)
10
+ end
11
+
6
12
  it 'reports close failure and retains the stream for cleanup retry' do
7
13
  handle = { device: :device, stream: :stream }
8
14
  allow(described_class).to receive(:SoapySDRDevice_deactivateStream).and_return(0)
@@ -39,8 +45,8 @@ describe PWN::FFI::SoapySDR do
39
45
  expect(PWN::FFI::SoapySDR).to respond_to :available?
40
46
  end
41
47
 
42
- it 'should report API info when libSoapySDR is present' do
43
- skip 'libSoapySDR not installed' unless PWN::FFI::SoapySDR.available?
48
+ it 'should report API info', :soapy_sdr_integration do
49
+ expect(described_class.available?).to be(true), 'Install libSoapySDR and make it visible to the dynamic loader (PWN_TEST_SOAPY_SDR=1).'
44
50
 
45
51
  info = PWN::FFI::SoapySDR.info
46
52
  expect(info[:available]).to eq(true)
@@ -3,6 +3,12 @@
3
3
  require 'spec_helper'
4
4
 
5
5
  describe PWN::FFI::Volk do
6
+ it 'handles a missing shared library without native calls' do
7
+ allow(described_class).to receive(:available?).and_return(false)
8
+ expect { described_class.unpack_s16le(data: '') }.to raise_error(RuntimeError, /libvolk not available/)
9
+ expect { described_class.accumulate(samples: []) }.to raise_error(RuntimeError, /libvolk not available/)
10
+ end
11
+
6
12
  it 'should display information for authors' do
7
13
  expect(PWN::FFI::Volk).to respond_to :authors
8
14
  end
@@ -15,8 +21,8 @@ describe PWN::FFI::Volk do
15
21
  expect(PWN::FFI::Volk).to respond_to :available?
16
22
  end
17
23
 
18
- it 'should unpack s16le to unit-range floats when libvolk is present' do
19
- skip 'libvolk not installed' unless PWN::FFI::Volk.available?
24
+ it 'should unpack s16le to unit-range floats', :volk_integration do
25
+ expect(described_class.available?).to be(true), 'Install libvolk and make it visible to the dynamic loader (PWN_TEST_VOLK=1).'
20
26
 
21
27
  raw = [0, 16_384, -16_384, 32_767].pack('s<*')
22
28
  out = PWN::FFI::Volk.unpack_s16le(data: raw)
@@ -27,8 +33,8 @@ describe PWN::FFI::Volk do
27
33
  expect(out[3]).to be_within(1e-3).of(1.0)
28
34
  end
29
35
 
30
- it 'should accumulate floats when libvolk is present' do
31
- skip 'libvolk not installed' unless PWN::FFI::Volk.available?
36
+ it 'should accumulate floats', :volk_integration do
37
+ expect(described_class.available?).to be(true), 'Install libvolk and make it visible to the dynamic loader (PWN_TEST_VOLK=1).'
32
38
 
33
39
  sum = PWN::FFI::Volk.accumulate(samples: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
34
40
  expect(sum).to be_within(1e-4).of(36.0)
@@ -5,20 +5,50 @@ require 'tmpdir'
5
5
  require 'digest'
6
6
 
7
7
  describe 'normalized binary analysis' do
8
- it 'executes the actual radare2 read-only JSON interfaces when installed' do
9
- require 'pwn/plugins/radare2'
10
- skip 'r2 unavailable' unless PWN::Plugins::BinaryAnalysis.available?(name: 'r2')
11
- result = PWN::Plugins::Radare2.analyze_all(path: '/bin/true')
12
- expect(result[:backend]).to eq('radare2')
13
- expect(result[:functions]).not_to be_empty
14
- expect(PWN::Plugins::Radare2.list_functions(path: '/bin/true')[:data]).not_to be_empty
15
- expect(PWN::Plugins::Radare2.disasm_function(path: '/bin/true', function: 'main')[:data]).to include('ops')
16
- expect(PWN::Plugins::Radare2.xrefs(path: '/bin/true', addr: 'main')[:data]).to be_a(Array)
17
- expect(PWN::Plugins::Radare2.strings(path: '/bin/true')[:data]).not_to be_empty
18
- expect(PWN::Plugins::Radare2.imports(path: '/bin/true')[:data]).not_to be_empty
8
+ it 'executes the actual radare2 read-only JSON interfaces on a controlled binary', :radare2_integration do
9
+ expect(PWN::Plugins::BinaryAnalysis.available?(name: 'r2')).to be(true),
10
+ 'PWN_TEST_RADARE2=1 requires radare2: install r2 and add it to PATH.'
11
+ expect(PWN::Plugins::BinaryAnalysis.available?(name: 'cc')).to be(true),
12
+ 'PWN_TEST_RADARE2=1 requires a C compiler: install cc and add it to PATH.'
13
+
14
+ Dir.mktmpdir('pwn-radare2-spec-') do |dir|
15
+ path = File.join(dir, 'fixture')
16
+ source = File.join(dir, 'fixture.c')
17
+ File.write(source, "#include <stdio.h>\nint main(void) { return puts(\"pwn radare2 fixture\") < 0; }\n")
18
+ output, status = Open3.capture2e('cc', '-O0', '-g', '-o', path, source, chdir: dir)
19
+ expect(status.success?).to be(true), "Unable to compile radare2 fixture with cc: #{output}"
20
+ sha = Digest::SHA256.file(path).hexdigest
21
+
22
+ result = PWN::Plugins::Radare2.analyze_all(path: path)
23
+ expect(result).to include(backend: 'radare2', status: 'ok', sha256: sha)
24
+ expect(result[:functions]).not_to be_empty
25
+ expect(PWN::Plugins::Radare2.list_functions(path: path)[:data]).not_to be_empty
26
+ disassembly = PWN::Plugins::Radare2.disasm_function(path: path, function: 'main')
27
+ expect(disassembly).to include(backend: 'radare2', status: 'ok')
28
+ expect(disassembly[:data]).to include('ops')
29
+ expect(disassembly[:data]['ops']).not_to be_empty
30
+ xrefs = PWN::Plugins::Radare2.xrefs(path: path, addr: 'main')
31
+ expect(xrefs).to include(backend: 'radare2', status: 'ok')
32
+ expect(xrefs[:data]).to be_a(Array)
33
+ expect(PWN::Plugins::Radare2.strings(path: path)[:data]).not_to be_empty
34
+ expect(PWN::Plugins::Radare2.imports(path: path)[:data]).not_to be_empty
35
+ expect(Digest::SHA256.file(path).hexdigest).to eq(sha)
36
+ end
37
+ end
38
+
39
+ it 'rejects command injection without requiring a native backend' do
40
+ expect(PWN::Plugins::BinaryAnalysis).not_to receive(:run)
19
41
  expect { PWN::Plugins::Radare2.disasm_function(path: '/bin/true', function: 'main;!id') }.to raise_error(ArgumentError)
20
42
  end
21
43
 
44
+ it 'falls back deterministically when radare2 is unavailable' do
45
+ allow(PWN::Plugins::BinaryAnalysis).to receive(:available?).with(name: 'r2').and_return(false)
46
+ result = PWN::Plugins::Radare2.analyze_all(path: '/bin/true')
47
+ expect(result).to include(backend: 'binutils', status: 'degraded')
48
+ expect(result[:strings]).not_to be_empty
49
+ expect(result[:warnings]).not_to be_empty
50
+ end
51
+
22
52
  it 'reports an honest Ghidra fallback and uses the requested hash cache directory' do
23
53
  Dir.mktmpdir do |dir|
24
54
  result = PWN::Plugins::GhidraHeadless.analyze(path: '/bin/true', backend: 'binutils', cache_dir: dir)
@@ -29,33 +29,58 @@ describe PWN::SDR::Decoder::DSP do
29
29
  PWN::SDR::Decoder::DSP.native = was
30
30
  end
31
31
 
32
- it 'should unpack s16le via Volk when available' do
33
- skip 'libvolk not installed' unless PWN::FFI.available?(mod: :Volk)
34
-
32
+ it 'should unpack s16le via Volk', :volk_integration do
35
33
  was = PWN::SDR::Decoder::DSP.native
34
+ expect(PWN::FFI.available?(mod: :Volk)).to be(true), 'Install libvolk and make it visible to the dynamic loader (PWN_TEST_VOLK=1).'
36
35
  PWN::SDR::Decoder::DSP.native = true
37
36
  raw = [0, 16_384, -16_384, 32_767].pack('s<*')
37
+ native_out = PWN::FFI::Volk.unpack_s16le(data: raw)
38
+ expect(PWN::FFI::Volk).to receive(:unpack_s16le).with(data: raw).and_call_original
38
39
  out = PWN::SDR::Decoder::DSP.unpack_s16le(data: raw)
40
+ expect(out).to eq(native_out)
39
41
  expect(out[1]).to be_within(1e-3).of(0.5)
40
42
  ensure
41
43
  PWN::SDR::Decoder::DSP.native = was
42
44
  end
43
45
 
44
- it 'should resample pure-ruby and liquid paths to comparable length' do
45
- samples = Array.new(200) { |i| Math.sin(2 * Math::PI * i / 40.0) }
46
+ it 'falls back to Ruby unpacking with native enabled but Volk absent' do
47
+ was = described_class.native
48
+ described_class.native = true
49
+ allow(PWN::FFI).to receive(:available?).with(mod: :Volk).and_return(false)
50
+ expect(PWN::FFI::Volk).not_to receive(:unpack_s16le)
51
+ raw = [0, 16_384, -16_384, 32_767].pack('s<*')
52
+ expect(described_class.unpack_s16le(data: raw)).to eq([0.0, 0.5, -0.5, 32_767 / 32_768.0])
53
+ ensure
54
+ described_class.native = was
55
+ end
46
56
 
47
- was = PWN::SDR::Decoder::DSP.native
48
- PWN::SDR::Decoder::DSP.native = false
49
- ruby_out = PWN::SDR::Decoder::DSP.resample(samples: samples, src_rate: 48_000, dst_rate: 24_000)
57
+ it 'falls back to Ruby resampling with native enabled but Liquid absent' do
58
+ was = described_class.native
59
+ samples = Array.new(200) { |i| Math.sin(2 * Math::PI * i / 40.0) }
60
+ described_class.native = false
61
+ ruby_out = described_class.resample(samples: samples, src_rate: 48_000, dst_rate: 24_000)
50
62
  expect(ruby_out.length).to eq(100)
51
63
 
52
- if PWN::FFI.available?(mod: :Liquid)
53
- PWN::SDR::Decoder::DSP.native = true
54
- liq_out = PWN::SDR::Decoder::DSP.resample(samples: samples, src_rate: 48_000, dst_rate: 24_000)
55
- # multi-stage resampler length can differ by a few samples due to delay
56
- expect(liq_out.length).to be_between(90, 120)
57
- end
64
+ allow(PWN::FFI).to receive(:available?).with(mod: :Liquid).and_return(false)
65
+ expect(PWN::FFI::Liquid).not_to receive(:resample)
66
+ described_class.native = true
67
+ expect(described_class.resample(samples: samples, src_rate: 48_000, dst_rate: 24_000)).to eq(ruby_out)
58
68
  ensure
59
- PWN::SDR::Decoder::DSP.native = was
69
+ described_class.native = was
70
+ end
71
+
72
+ it 'resamples using Liquid to a comparable length', :liquid_integration do
73
+ was = described_class.native
74
+ expect(PWN::FFI.available?(mod: :Liquid)).to be(true), 'Install libliquid and make it visible to the dynamic loader (PWN_TEST_LIQUID=1).'
75
+ samples = Array.new(200) { |i| Math.sin(2 * Math::PI * i / 40.0) }
76
+ described_class.native = true
77
+ native_out = PWN::FFI::Liquid.resample(samples: samples, rate: 0.5)
78
+ expect(PWN::FFI::Liquid).to receive(:resample).with(samples: samples, rate: 0.5).and_call_original
79
+ liq_out = described_class.resample(samples: samples, src_rate: 48_000, dst_rate: 24_000)
80
+ expect(liq_out).to eq(native_out)
81
+ # Multi-stage resampler length can differ due to delay.
82
+ expect(liq_out.length).to be_between(90, 120)
83
+ ensure
84
+ described_class.native = was
60
85
  end
61
86
  end
data/spec/spec_helper.rb CHANGED
@@ -21,6 +21,12 @@ RSpec.configure do |config|
21
21
  config.filter_run_excluding :local_embeddings unless ENV['PWN_TEST_EMBED_ENDPOINT'] && ENV['PWN_SQLITE_VEC_EXTENSION']
22
22
  config.filter_run_excluding :proxmark3_integration unless ENV['PWN_TEST_PROXMARK3'] == '1'
23
23
  config.filter_run_excluding :rtl433_integration unless ENV['PWN_TEST_RTL433'] == '1'
24
+ config.filter_run_excluding :liquid_integration unless ENV['PWN_TEST_LIQUID'] == '1'
25
+ config.filter_run_excluding :radare2_integration unless ENV['PWN_TEST_RADARE2'] == '1'
26
+ %w[fftw volk rtl_sdr adalm_pluto soapy_sdr hack_rf].each do |backend|
27
+ config.filter_run_excluding :"#{backend}_integration" unless ENV["PWN_TEST_#{backend.upcase}"] == '1'
28
+ end
29
+ config.filter_run_excluding :rtl_sdr_hardware unless ENV['PWN_TEST_RTL_SDR_HARDWARE'] == '1'
24
30
 
25
31
  next if ENV['PWN_SPEC_VERBOSE']
26
32
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pwn
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.729
4
+ version: 0.5.731
5
5
  platform: ruby
6
6
  authors:
7
7
  - 0day Inc.
@@ -519,14 +519,14 @@ dependencies:
519
519
  requirements:
520
520
  - - '='
521
521
  - !ruby/object:Gem::Version
522
- version: 0.0.175
522
+ version: 0.0.176
523
523
  type: :development
524
524
  prerelease: false
525
525
  version_requirements: !ruby/object:Gem::Requirement
526
526
  requirements:
527
527
  - - '='
528
528
  - !ruby/object:Gem::Version
529
- version: 0.0.175
529
+ version: 0.0.176
530
530
  - !ruby/object:Gem::Dependency
531
531
  name: metasm
532
532
  requirement: !ruby/object:Gem::Requirement
@@ -547,14 +547,14 @@ dependencies:
547
547
  requirements:
548
548
  - - '='
549
549
  - !ruby/object:Gem::Version
550
- version: 2.25.0
550
+ version: 2.26.0
551
551
  type: :runtime
552
552
  prerelease: false
553
553
  version_requirements: !ruby/object:Gem::Requirement
554
554
  requirements:
555
555
  - - '='
556
556
  - !ruby/object:Gem::Version
557
- version: 2.25.0
557
+ version: 2.26.0
558
558
  - !ruby/object:Gem::Dependency
559
559
  name: msfrpc-client
560
560
  requirement: !ruby/object:Gem::Requirement
@@ -953,14 +953,14 @@ dependencies:
953
953
  requirements:
954
954
  - - '='
955
955
  - !ruby/object:Gem::Version
956
- version: 1.90.0
956
+ version: 1.91.0
957
957
  type: :runtime
958
958
  prerelease: false
959
959
  version_requirements: !ruby/object:Gem::Requirement
960
960
  requirements:
961
961
  - - '='
962
962
  - !ruby/object:Gem::Version
963
- version: 1.90.0
963
+ version: 1.91.0
964
964
  - !ruby/object:Gem::Dependency
965
965
  name: rubocop-rake
966
966
  requirement: !ruby/object:Gem::Requirement