interceptor-multilang 1.0.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2671416965532d4e88dd3a648598760b36d40ab31a4c800d791703c6d9f5a70c
4
+ data.tar.gz: 0bc8dffeb21a2d025c328279c07fa591d25ce7e21eff3bb7e51f5e25cedf3963
5
+ SHA512:
6
+ metadata.gz: ecc64dc856756fed9435db3292c247475d21ee657a04c2fc9ed7ebf6c968b6b2229eb3d34372ec370c33d58ca3e4018a97c85be97f080d1226f693b65f067e2a
7
+ data.tar.gz: 7aedd14bd9f4b10040e452392b9a6327dd4b99a7599e1bbeb81bf454d02d528a7decfc6e9725407c758474fb18e6306c04af4bf2f50bf446d298d0e3eb305f7c
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ # 1.0.0
2
+
3
+ First direct native SDK with device enumeration, input batches, HID metadata, edits, injection, cancellation and bundled documentation. No driver is installed by this package.
data/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 TitanCSA
4
+
5
+ This license applies to the user-mode source code, examples, build scripts,
6
+ documentation and precompiled user-mode recovery binaries in this repository.
7
+ It does not apply to the prebuilt kernel
8
+ driver, driver catalogs, or INF files under InstallerUI/Package/Payload; those files are
9
+ covered by licenses/BINARY-LICENSE.txt. Third-party names and projects retain their own
10
+ licenses and trademarks.
11
+
12
+ Permission is hereby granted, free of charge, to any person obtaining a copy
13
+ of this software and associated documentation files (the "Software"), to deal
14
+ in the Software without restriction, including without limitation the rights
15
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
16
+ copies of the Software, and to permit persons to whom the Software is
17
+ furnished to do so, subject to the following conditions:
18
+
19
+ The above copyright notice and this permission notice shall be included in all
20
+ copies or substantial portions of the Software.
21
+
22
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
23
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
24
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
25
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
26
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
27
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
28
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,46 @@
1
+ # Interceptor MultiLang for Ruby
2
+
3
+ Direct Fiddle bindings for Windows x64 Ruby 3.1+, with the native DLL bundled.
4
+ No Python runtime, compiler or subprocess bridge is required.
5
+
6
+ ```sh
7
+ gem install interceptor-multilang
8
+ ```
9
+
10
+ The installation command is available after RubyGems publication.
11
+
12
+ ```ruby
13
+ require 'interceptor_multilang'
14
+ InterceptorMultiLang::Interceptor.open do |io|
15
+ p io.info
16
+ end
17
+ ```
18
+
19
+ Opening a client is lazy. For capture, separately install the combined Interceptor
20
+ 1.4.22.0 driver and run the script as administrator. No driver is installed by gem.
21
+ Only one native SDK context may be open per process; use the block form or close
22
+ the client in `ensure`. Receive is synchronous and releases Ruby's GVL; cancel
23
+ can interrupt a receive from another Ruby thread.
24
+
25
+ ```ruby
26
+ batch = io.receive(timeout: 1000)
27
+ batch.each do |input|
28
+ input.field('y').value *= -1 if input.kind == 'mouse'
29
+ end
30
+ io.send(batch)
31
+ ```
32
+
33
+ Always forward the complete batch or deliberately block/discard events.
34
+ `batch[0].blocked = true` suppresses an event. A new receive invalidates older
35
+ batches, including failed receives. Partial-send errors expose the accepted count;
36
+ retrying `send` on the same current batch forwards only the remainder.
37
+
38
+ The API includes `initialize_devices`, `info`, `devices`, `receive`, `send`,
39
+ `discard`, `cancel`, `descriptor(token, report: true)`,
40
+ `inject('mouse', {'x' => 200})` and `close`.
41
+ HID fields retain collection, usage and element metadata; duplicate names are not
42
+ collapsed. Sequence/timestamp values are exact Ruby integers. Binary data is a byte
43
+ string; its native length must not change when editing a received field.
44
+
45
+ [API and event fields](docs/API.md), examples and MIT [LICENSE](LICENSE) are bundled.
46
+ Driver demo restrictions and licensing remain separate from this SDK.
data/docs/API.md ADDED
@@ -0,0 +1,75 @@
1
+ # API and examples
2
+
3
+ The short examples in `examples/` invert mouse Y and forward every other input unchanged. Install the matching driver and run them as administrator. `initialize()` is available for newly connected devices; check `ready` and `reboot_required` before capturing.
4
+
5
+ ## Shared behavior
6
+
7
+ `receive` waits in milliseconds. Python/Node default `None`/`null`, Rust `None`, and .NET/Java/Go `-1` mean infinite. Zero is nonblocking. Timeout or cancellation returns an empty batch. Native C uses `UINT32_MAX` for infinite.
8
+
9
+ Receive consumes input. Until the batch is sent or discarded, another receive is rejected. A batch has fixed membership and source order: edit its state or set `blocked`; do not resize/reorder it. Send infers counts and returns the number forwarded. Blocked events are excluded. On partial failure, the error includes `accepted`; retry the same batch to send only the remaining entries. There is no hidden retry loop.
10
+
11
+ Native errors carry a Windows code and an English explanation. Python exposes `Error.code/accepted`, Node errors have `code/accepted`, .NET has `InterceptorException.Code/Accepted`, Java `Interceptor.Error.code/accepted`, Go `*interceptor.Error`, and Rust `interceptor::Error`. After a driver revocation, close the client before explicitly starting a new capture; an expired demo still requires Windows Restart.
12
+
13
+ Managed input objects are detached snapshots. Their native commit references are checked against the client and current batch generation before use. Closing a client or attempting a new receive can invalidate the prior batch's send ticket. Ordinary state inspection remains safe; sending an old batch raises an error. Native C views are borrowed: use the lifetime rules in the header and do not access views concurrently with receive/close.
14
+
15
+ ## Input identity
16
+
17
+ Each input has a kind (`keyboard`, `mouse`, `hid`, `xinput`, `xinputhid`, `gip`), a capture-scoped string `token`, sequence, QPC timestamp and a mutable blocked flag. Metadata is read-only. Tokens are not persistent hardware serial numbers; obtain them again for each capture. Sequence/timestamp are 64-bit: JavaScript uses BigInt; Java `long` preserves their unsigned bit pattern.
18
+
19
+ `devices()` lists sources from the driver's capture, not every PnP child node. It opens capture and starts the demo allowance if not already started. `info()` is a read-only query. Its `active` field reports driver-wide capture activity, while `device_count` is the size of this client's last device enumeration (zero before enumeration). Its legacy `queued` field covers keyboard, mouse and HID queues; it is not an all-transport queue total. Use the returned batches for actual input activity.
20
+
21
+ ## State fields
22
+
23
+ | Kind | Scalar fields |
24
+ | --- | --- |
25
+ | Keyboard | `scan_code`, `flags`, `information` |
26
+ | Mouse | `x`, `y`, `flags`, `buttons`, signed `wheel`, `raw_buttons`, `information` |
27
+ | XInput / GIP | read-only `packet`, `buttons`, `left_trigger`, `right_trigger`, `left_x`, `left_y`, `right_x`, `right_y`, `extra_buttons` |
28
+
29
+ Keyboard flags preserve make/break and extended-key information. Mouse coordinates are relative unless the native absolute flag is set. Mouse `buttons` contains transition flags; gamepad `buttons` is a state bitmask. XInput and GIP use their native button positions and trigger ranges: XInput 0-255, GIP 0-1023, stick axes signed 16-bit. Do not interchange the protocols' button masks.
30
+
31
+ HID/XInputHID expose dynamic `axes`, `triggers`, `hats`, `values`, `buttons`, and `binary_values`, plus the complete report. Python/Node use `input.state.axes`; .NET uses `input.State.Axes`; Java uses `input.state.axes`; Rust uses `input.state.axes`; Go uses `input.HID.Axes`. Scalar maps are `state` in Python/Node, `State` in .NET/Go, `state.scalars` in Java/Rust.
32
+
33
+ HID scalar fields include raw value, minimum/maximum, physical range, normalized value and validity, usage page/usage, collection, report ID, data index and element. Repeated X usages remain distinct. Button `pressed` is mutable. Wide values retain their packed bytes in `data`. Flags: 1 normalized valid, 2 relative, 4 has null, 8 is null, 16 identified trigger, 32 read-only.
34
+
35
+ Edit a scalar's **raw value or normalized value**. Absolute axis normalization is -1..1; identified analog triggers use 0..1. Relative/null/constant-range values may have no valid normalization. An unchanged null hat may be outside its declared ordinary angle range and is preserved. Contradictory edits or values outside valid ranges are rejected. Report and binary data lengths are immutable.
36
+
37
+ HID state is the current report. Check `decode_complete` and `decode_error` before relying on decoded controls. Unknown bytes remain in the original report when forwarded unchanged; proprietary meaning is not guessed.
38
+
39
+ ## Injection and descriptors
40
+
41
+ Create input for an existing captured source with `inject`. Supplying a token selects that exact source. Omitting it selects the first device of the requested kind. The normal send operation always retains the original captured source.
42
+
43
+ ```python
44
+ with Interceptor() as io:
45
+ mouse = next(d for d in io.devices() if d.kind == "mouse")
46
+ io.inject("mouse", token=mouse.token, x=200, y=0)
47
+ ```
48
+
49
+ Python supports named state keywords; Node accepts `{kind, token?, state, report?}`; .NET accepts `Inject(kind, stateDictionary, token?, report?)`; Java accepts `inject(kind, stateMap, token, report)`; Rust accepts kind, a slice of name/value pairs, optional token and optional report. Go uses a state map and an optional `Injection{Token: ..., Report: ...}`. Raw HID injection requires a valid report for that device. GIP synthetic input uses named standard state; captured GIP preserves its original complete report and extensions.
50
+
51
+ `descriptor(token, report=false)` returns common descriptor data. `report=true` requests the **actual raw HID report descriptor**, where accessible. XUSB/GIP return not applicable. An unsupported descriptor transport returns an error; the wrapper does not substitute Windows preparsed data for a raw HID report descriptor. Formats: 0 identity, 1 HID preparsed data, 2 GIP metadata, 3 raw HID report descriptor.
52
+
53
+ ## Java
54
+
55
+ From the extracted release folder, using JDK 22+:
56
+
57
+ ```powershell
58
+ javac -cp bindings/java/Interceptor.jar -d examples/java examples/java/Remap.java
59
+ java --enable-native-access=ALL-UNNAMED -cp "bindings/java/Interceptor.jar;examples/java" Remap
60
+ ```
61
+
62
+ The native-access flag is required by the JDK's Foreign Function and Memory API. No JNI build or third-party Java dependency is needed. Keep the release's `runtimes/win-x64/` folder, or place the DLL beside the JAR.
63
+
64
+ ## Add to another application
65
+
66
+ - Python: install the bundled Windows x64 wheel in the application's environment.
67
+ - Node/TypeScript: `npm install ./packages/titancsa-interceptor-multilang-1.0.0.tgz`, then import `@titancsa/interceptor-multilang`.
68
+ - .NET: use the local `TitanCSA.Interceptor` NuGet package or reference `bindings/dotnet/Interceptor.csproj`.
69
+ - Java/Kotlin: add `bindings/java/Interceptor.jar` to the classpath.
70
+ - Go: import the source module in `bindings/go`; distribute `InterceptorBridge.dll` beside the final EXE.
71
+ - Rust: use a path dependency on `bindings/rust`. Its build script links the import library and copies the DLL beside the executable. `INTERCEPTOR_LIB_DIR` can select another directory containing matching DLL/LIB files.
72
+ - C: link `InterceptorBridge.lib`, include `InterceptorBridge.h` and distribute the DLL beside the EXE.
73
+ - C++: compile `sdk/Interceptor.cpp` once and include `sdk/Interceptor.h`; the unchanged upstream SDK remains available directly.
74
+
75
+ For development/testing, `INTERCEPTOR_NATIVE_PATH` can explicitly select a matching native DLL. Node uses `INTERCEPTOR_NODE_PATH` for a matching addon. Do not select the synthetic test backend for normal capture. Paths are resolved to supplied files; installation does not add global PATH entries or a wrapper service.
data/examples/info.rb ADDED
@@ -0,0 +1,2 @@
1
+ require 'interceptor_multilang'
2
+ InterceptorMultiLang::Interceptor.open { |io| p io.info }
@@ -0,0 +1,205 @@
1
+ # frozen_string_literal: true
2
+ require 'fiddle'
3
+ require 'thread'
4
+
5
+ module InterceptorMultiLang
6
+ VERSION = '1.0.0'
7
+ KINDS = ['', 'keyboard', 'mouse', 'hid', 'xinput', 'xinputhid', 'gip'].freeze
8
+ class NativeError < StandardError
9
+ attr_reader :code, :accepted
10
+ def initialize(code, message, accepted = 0)
11
+ @code, @accepted = code, accepted
12
+ super(message)
13
+ end
14
+ end
15
+ Field = Struct.new(:value, :normalized, :data, :metadata, keyword_init: true)
16
+ Input = Struct.new(:kind, :token, :sequence, :timestamp, :fields, :metadata, :blocked, keyword_init: true) do
17
+ def field(name)
18
+ found = fields.select { |f| f.metadata[:name] == name }
19
+ raise ArgumentError, 'Field is absent or ambiguous; use descriptor metadata' unless found.length == 1
20
+ found.first
21
+ end
22
+ end
23
+ class Batch
24
+ include Enumerable
25
+ attr_reader :events
26
+ def initialize(events) = @events = events.freeze
27
+ def each(&block) = @events.each(&block)
28
+ def [](index) = @events[index]
29
+ def size = @events.size
30
+ alias length size
31
+ end
32
+
33
+ # All pointer/layout operations stay private; callers edit copied field values.
34
+ class Native
35
+ I = Fiddle::TYPE_INT
36
+ Q = Fiddle::TYPE_LONG_LONG
37
+ P = Fiddle::TYPE_VOIDP
38
+ SPECS = {
39
+ version: [], sizeof: [I], open: [P,P], close: [Q,P], initialize: [Q,P,P],
40
+ info_get: [Q,P,P], devices_get: [Q,P,P], receive: [Q,I,P,P], send: [Q,Q,P],
41
+ discard: [Q,Q,P], cancel: [Q,P], descriptor: [Q,P,I,P,P], inject: [Q,P,I,P]
42
+ }.freeze
43
+ @libraries = {}
44
+ @libraries_mutex = Mutex.new
45
+ def self.load(path)
46
+ @libraries_mutex.synchronize { @libraries[path] ||= new(path) }
47
+ end
48
+ def initialize(path)
49
+ @library = Fiddle.dlopen(path)
50
+ @functions = SPECS.to_h { |name, args| [name, Fiddle::Function.new(@library["icb_#{name}"], args, I)] }
51
+ end
52
+ def call(name, *arguments) = @functions.fetch(name).call(*arguments)
53
+ def self.buffer(length)
54
+ pointer = Fiddle::Pointer.malloc([length,1].max, Fiddle::RUBY_FREE)
55
+ pointer[0,[length,1].max] = "\0" * [length,1].max
56
+ pointer
57
+ end
58
+ def self.u32(p, off) = p[off,4].unpack1('L<')
59
+ def self.u64(p, off) = p[off,8].unpack1('Q<')
60
+ def self.i64(p, off) = p[off,8].unpack1('q<')
61
+ def self.pointer(p, off) = Fiddle::Pointer.new(u64(p,off))
62
+ def self.text(p) = p.null? ? '' : p.to_s.force_encoding(Encoding::UTF_8)
63
+ def self.signed64(value) = value >= (1 << 63) ? value - (1 << 64) : value
64
+ end
65
+
66
+ class Interceptor
67
+ def initialize(library_path: nil)
68
+ raise LoadError, 'Interceptor requires Windows x64 Ruby' unless /mswin|mingw/ =~ RUBY_PLATFORM && Fiddle::SIZEOF_VOIDP == 8
69
+ path = library_path || ENV['INTERCEPTOR_NATIVE_PATH'] || File.expand_path('../native/InterceptorBridge.dll', __dir__)
70
+ @native = Native.load(File.expand_path(path)); @mutex = Mutex.new; @session = 0; @current = nil
71
+ raise LoadError, 'Incompatible native ABI' unless @native.call(:version) == 1 && [120,64,24,40,776,56].each_with_index.all? { |size,index| @native.call(:sizeof,index+1) == size }
72
+ out = Native.buffer(8); checked(:open,out); @session = Native.u64(out,0)
73
+ end
74
+ def checked(name, *arguments)
75
+ error = Native.buffer(776)
76
+ code = @native.call(name, *arguments, error)
77
+ raise NativeError.new(code, error[8,768].split("\0",2).first.force_encoding('UTF-8'), Native.u32(error,4)) unless code.zero?
78
+ Native.u32(error,4)
79
+ end
80
+ private :checked
81
+ def active
82
+ raise IOError, 'Interceptor is closed' if @session.zero?
83
+ Native.signed64(@session)
84
+ end
85
+ private :active
86
+ def initialize_devices
87
+ @mutex.synchronize do
88
+ out = Native.buffer(24); checked(:initialize,active,out)
89
+ %i[ready reboot_required associated already_associated skipped].each_with_index.to_h { |name,index| [name,Native.u32(out,index*4)] }
90
+ end
91
+ end
92
+ def info
93
+ @mutex.synchronize do
94
+ out = Native.buffer(56); checked(:info_get,active,out)
95
+ result = %i[installed active trial_state input_mask controller_mask device_count queued last_reset].each_with_index.to_h { |name,index| [name,Native.u32(out,index*4)] }
96
+ %i[remaining_ms timestamp_frequency generation].each_with_index { |name,index| result[name] = Native.u64(out,32+index*8) }
97
+ result
98
+ end
99
+ end
100
+ def devices
101
+ @mutex.synchronize do
102
+ out = Native.buffer(8); checked(:devices_get,active,out); list = Native.pointer(out,0); items = Native.pointer(list,8)
103
+ Array.new(Native.u32(list,0)) do |i|
104
+ d = items + i*40
105
+ result = { kind: KINDS[Native.u32(d,0)], id: Native.u32(d,4) }
106
+ %i[vid pid usage_page usage].each_with_index { |name,index| result[name] = d[8+index*2,2].unpack1('S<') }
107
+ %i[token name hardware_id].each_with_index { |name,index| result[name] = Native.text(Native.pointer(d,16+index*8)) }
108
+ result
109
+ end
110
+ end
111
+ end
112
+ def receive(timeout: nil)
113
+ raise ArgumentError, 'Invalid timeout' unless timeout.nil? || timeout.is_a?(Integer) && timeout.between?(0,0xfffffffe)
114
+ @mutex.synchronize do
115
+ @current = nil; @header = nil
116
+ out = Native.buffer(8); checked(:receive,active,timeout.nil? ? -1 : (timeout >= (1 << 31) ? timeout - (1 << 32) : timeout),out)
117
+ @header = Native.pointer(out,0); @ticket = Native.u64(@header,0); @views = Native.pointer(@header,16)
118
+ events = Array.new(Native.u32(@header,8)) do |i|
119
+ e = @views + i*64; fs = Native.pointer(e,32)
120
+ fields = Array.new(Native.u32(e,40)) do |j|
121
+ f = fs + j*120
122
+ m = {name: Native.text(Native.pointer(f,0)), minimum: Native.i64(f,16), maximum: Native.i64(f,24), physical_minimum: Native.i64(f,32), physical_maximum: Native.i64(f,40)}
123
+ %i[group index usage_page usage collection report_id data_index element flags units units_exponent bit_size].each_with_index { |name,index| m[name] = Native.u32(f,56+index*4) }
124
+ bytes = Native.u32(f,112)
125
+ Field.new(value: Native.i64(f,8), normalized: f[48,8].unpack1('E'), data: bytes.zero? ? ''.b : Native.pointer(f,104)[0,bytes], metadata: m.freeze)
126
+ end.freeze
127
+ Input.new(kind: KINDS[Native.u32(e,0)], token: Native.text(Native.pointer(e,24)), sequence: Native.u64(e,8), timestamp: Native.u64(e,16), fields: fields,
128
+ metadata: %i[decode_error decode_complete parser_status device].each_with_index.to_h { |name,index| [name,Native.u32(e,44+index*4)] }.freeze, blocked: Native.u32(e,4) != 0)
129
+ end
130
+ @issued_fields = events.map(&:fields); @current = Batch.new(events)
131
+ end
132
+ end
133
+ def validate(batch)
134
+ active
135
+ raise ArgumentError, 'Stale or foreign batch' unless batch.equal?(@current) && @header && Native.u64(@header,0) == @ticket && batch.size == Native.u32(@header,8)
136
+ batch.each_with_index { |event,index| raise ArgumentError, 'Do not replace/reorder fields' unless event.fields.equal?(@issued_fields[index]) }
137
+ end
138
+ private :validate
139
+ def send(batch)
140
+ @mutex.synchronize do
141
+ validate(batch)
142
+ batch.each_with_index do |event,i|
143
+ fs = Native.pointer(@views+i*64,32)
144
+ event.fields.each_with_index do |field,j|
145
+ raise ArgumentError, 'Field value must fit signed 64 bits' unless field.value.is_a?(Integer) && field.value.between?(-(1<<63),(1<<63)-1)
146
+ raise ArgumentError, 'Normalized values must be finite' unless field.normalized.is_a?(Numeric) && field.normalized.finite?
147
+ raise ArgumentError, 'Do not resize binary/report data' unless field.data.is_a?(String) && field.data.bytesize == Native.u32(fs+j*120,112)
148
+ end
149
+ end
150
+ batch.each_with_index do |event,i|
151
+ e = @views+i*64; e[4,4] = [event.blocked ? 1 : 0].pack('L<'); fs = Native.pointer(e,32)
152
+ event.fields.each_with_index do |field,j|
153
+ f=fs+j*120;f[8,8]=[field.value].pack('q<');f[48,8]=[field.normalized].pack('E')
154
+ Native.pointer(f,104)[0,field.data.bytesize]=field.data unless field.data.empty?
155
+ end
156
+ end
157
+ checked(:send,active,Native.signed64(@ticket))
158
+ end
159
+ end
160
+ def discard(batch) = @mutex.synchronize { validate(batch); checked(:discard,active,Native.signed64(@ticket)); nil }
161
+ def cancel = (checked(:cancel,active); nil)
162
+ def descriptor(token, report: false)
163
+ raise ArgumentError, 'Token contains NUL' if token.include?("\0")
164
+ @mutex.synchronize do
165
+ out=Native.buffer(24);checked(:descriptor,active,token+"\0",report ? 1 : 0,out);length=Native.u32(out,8)
166
+ { applicable: Native.u32(out,0)!=0, format: Native.u32(out,4), data: length.zero? ? ''.b : Native.pointer(out,16)[0,length] }
167
+ end
168
+ end
169
+ def inject(kind, state = {}, token: nil, report: nil)
170
+ k=KINDS.index(kind);raise ArgumentError, 'Unknown input kind' unless k && k>0
171
+ @mutex.synchronize do
172
+ count=state.size+(report.nil? ? 0 : 1);fields=Native.buffer(count*120);strings=[]
173
+ state.each_with_index do |(name,value),i|
174
+ raise ArgumentError, 'Invalid state name or value' unless name.is_a?(String) && !name.include?("\0") && value.is_a?(Integer) && value.between?(-(1<<63),(1<<63)-1)
175
+ name_pointer=Fiddle::Pointer[name+"\0"];strings << name_pointer;f=fields+i*120;f[0,8]=[name_pointer.to_i].pack('Q<');f[8,8]=[value].pack('q<')
176
+ end
177
+ unless report.nil?
178
+ raise ArgumentError, 'Report must be a byte string' unless report.is_a?(String)
179
+ name=Fiddle::Pointer["report\0"];data=Fiddle::Pointer[report];strings.push(name,data);f=fields+state.size*120;f[0,8]=[name.to_i].pack('Q<');f[56,4]=[7].pack('L<');f[104,8]=[data.to_i].pack('Q<');f[112,4]=[report.bytesize].pack('L<')
180
+ end
181
+ event=Native.buffer(64);event[0,4]=[k].pack('L<');event[32,8]=[fields.to_i].pack('Q<');event[40,4]=[count].pack('L<')
182
+ if token
183
+ raise ArgumentError, 'Token contains NUL' if token.include?("\0")
184
+ t=Fiddle::Pointer[token+"\0"];strings << t;event[24,8]=[t.to_i].pack('Q<')
185
+ end
186
+ checked(:inject,active,event,1)
187
+ end
188
+ end
189
+ def close
190
+ return if @session.zero?
191
+ begin; cancel; rescue NativeError; end
192
+ @mutex.synchronize do
193
+ unless @session.zero?
194
+ id=@session;@session=0;@current=nil;@header=nil;checked(:close,Native.signed64(id))
195
+ end
196
+ end
197
+ nil
198
+ end
199
+ def self.open(**options)
200
+ io=new(**options)
201
+ return io unless block_given?
202
+ begin; yield io; ensure; io.close; end
203
+ end
204
+ end
205
+ end
Binary file
data/test/sdk_test.rb ADDED
@@ -0,0 +1,22 @@
1
+ require_relative '../lib/interceptor_multilang'
2
+ def verify(condition, message); raise message unless condition; end
3
+ io=InterceptorMultiLang::Interceptor.new
4
+ begin
5
+ if ENV['INTERCEPTOR_TEST_BACKEND']=='1'
6
+ verify(io.initialize_devices[:ready]==1,'initialize');devices=io.devices;verify(devices.size==6,'devices');verify(devices[0][:name].include?("\u00e9"),'UTF-8')
7
+ batch=io.receive(timeout:0);verify(batch.size==6,'six event kinds');verify(batch[0].sequence==0x8000000000000001,'uint64')
8
+ batch[0].field('scan_code').value=0x30;batch[1].field('y').value *= -1
9
+ axes=batch[2].fields.select{|f|f.metadata[:group]==1};verify(axes.size==2,'repeated HID fields');axes[0].value=50;axes[0].normalized=0.5
10
+ batch[5].blocked=true;verify(io.send(batch)==5,'send/block');verify(io.send(batch)==0,'retry');verify(io.inject('mouse',{'x'=>200})==1,'inject')
11
+ newer=io.receive(timeout:0);begin;io.send(batch);raise 'stale accepted';rescue ArgumentError;end;io.discard(newer)
12
+ api=Fiddle.dlopen(ENV.fetch('INTERCEPTOR_NATIVE_PATH'));mode=Fiddle::Function.new(api['icb_test_mode'],[Fiddle::TYPE_INT],Fiddle::TYPE_VOID)
13
+ mode.call(1);result=nil;t=Thread.new {result=io.receive};sleep 0.05;io.cancel;t.join(2);verify(!t.alive? && result.size==0,'cancellation');mode.call(0)
14
+ io.close;other=InterceptorMultiLang::Interceptor.new
15
+ begin;begin;other.send(newer);raise 'foreign accepted';rescue ArgumentError;end;ensure;other.close;end
16
+ puts 'Ruby fixture passed: six families, UTF-8, uint64, edits, HID, send/block, retry, injection, stale/foreign and threaded cancellation'
17
+ else
18
+ puts 'Ruby bundled production native DLL lazy open/close passed'
19
+ end
20
+ ensure
21
+ io.close
22
+ end
metadata ADDED
@@ -0,0 +1,71 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: interceptor-multilang
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - TitanCSA
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: fiddle
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '1.1'
19
+ - - "<"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.0'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '1.1'
29
+ - - "<"
30
+ - !ruby/object:Gem::Version
31
+ version: '2.0'
32
+ description: Fiddle bindings with native DLL, API docs and examples. The matching
33
+ input driver is installed separately.
34
+ executables: []
35
+ extensions: []
36
+ extra_rdoc_files: []
37
+ files:
38
+ - CHANGELOG.md
39
+ - LICENSE
40
+ - README.md
41
+ - docs/API.md
42
+ - examples/info.rb
43
+ - lib/interceptor_multilang.rb
44
+ - native/InterceptorBridge.dll
45
+ - test/sdk_test.rb
46
+ homepage: https://github.com/TitanCSA/Interceptor-MultiLang
47
+ licenses:
48
+ - MIT
49
+ metadata:
50
+ source_code_uri: https://github.com/TitanCSA/Interceptor-MultiLang
51
+ documentation_uri: https://github.com/TitanCSA/Interceptor-MultiLang/blob/main/docs/API.md
52
+ bug_tracker_uri: https://github.com/TitanCSA/Interceptor-MultiLang/issues
53
+ allowed_push_host: https://rubygems.org
54
+ rdoc_options: []
55
+ require_paths:
56
+ - lib
57
+ required_ruby_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '3.1'
62
+ required_rubygems_version: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
67
+ requirements: []
68
+ rubygems_version: 4.0.20
69
+ specification_version: 4
70
+ summary: Direct Windows x64 Interceptor input SDK for Ruby
71
+ test_files: []