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 +7 -0
- data/CHANGELOG.md +3 -0
- data/LICENSE +28 -0
- data/README.md +46 -0
- data/docs/API.md +75 -0
- data/examples/info.rb +2 -0
- data/lib/interceptor_multilang.rb +205 -0
- data/native/InterceptorBridge.dll +0 -0
- data/test/sdk_test.rb +22 -0
- metadata +71 -0
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
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,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: []
|