twib 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/LICENSE +5 -0
- data/README.md +16 -3
- data/lib/twib.rb +55 -11
- data/lib/twib/interface.rb +24 -10
- data/lib/twib/interfaces/ITwibDebugger.rb +56 -13
- data/lib/twib/interfaces/ITwibDeviceInterface.rb +6 -0
- data/lib/twib/interfaces/ITwibMetaInterface.rb +5 -0
- data/lib/twib/version.rb +1 -1
- data/twib.gemspec +2 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b9015c1f3220bfaeda563a3f8c1cc515b29379dd
|
4
|
+
data.tar.gz: 35e11df95809753993e09f2374e710bdb0459d09
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7924978593a63d765464260639f158aa4cc1b3690851635e77b7b2fd1bf367aef5c6d0944453bf195a21a1268fddb5fe246b8e57b038facd5024c5c8f307815a
|
7
|
+
data.tar.gz: def10e5a85b65b522bdd401718c12df4ac01fc37e457a1207b929068dddbe307fe474be82ef1c632575294a254cef8e09737c868065e0586875a6367b2685cb1
|
data/Gemfile.lock
CHANGED
data/LICENSE
ADDED
@@ -0,0 +1,5 @@
|
|
1
|
+
Copyright 2018 misson20000
|
2
|
+
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
4
|
+
|
5
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# ruby-twib
|
2
|
+
[![Gem Version](https://badge.fury.io/rb/twib.svg)](https://badge.fury.io/rb/twib)
|
2
3
|
|
3
4
|
[Twili](https://github.com/misson20000/twili) bridge client for Ruby, providing a way for Ruby applications to send requests to a Twili device via twibd.
|
4
5
|
|
@@ -22,19 +23,21 @@ Or install it yourself as:
|
|
22
23
|
|
23
24
|
See the instructions on the [Twili repository](https://github.com/misson20000/twili#twili) for how to set up Twili and twibd.
|
24
25
|
|
25
|
-
First, create a Twib::TwibConnection. There is a convenience method
|
26
|
+
First, create a [Twib::TwibConnection](https://www.rubydoc.info/gems/twib/Twib/TwibConnection). There is a convenience method [Twib::TwibConnection::connect_unix](https://www.rubydoc.info/gems/twib/Twib/TwibConnection#connect_unix-class_method).
|
26
27
|
```ruby
|
28
|
+
> require "twib"
|
29
|
+
=> true
|
27
30
|
> tc = Twib::TwibConnection.connect_unix
|
28
31
|
=> #<Twib::TwibConnection:...>
|
29
32
|
```
|
30
33
|
|
31
|
-
After that, you can list the available devices with
|
34
|
+
After that, you can list the available devices with [TwibConnection#list_devices](https://www.rubydoc.info/gems/twib/Twib/TwibConnection#list_devices-instance_method), which returns an array of hashes identifying each device.
|
32
35
|
```ruby
|
33
36
|
> tc.list_devices
|
34
37
|
[{"device_id"=>507914862, ...}, ...]
|
35
38
|
```
|
36
39
|
|
37
|
-
Use
|
40
|
+
Use [TwibConnection#open_device](https://www.rubydoc.info/gems/twib/Twib/TwibConnection#open_device-instance_method) to get a device's [ITwibDeviceInterface](https://www.rubydoc.info/gems/twib/Twib/Interfaces/ITwibDeviceInterface).
|
38
41
|
```ruby
|
39
42
|
> tc.open_device(507914862)
|
40
43
|
=> #<Twib::Interfaces::ITwibDeviceInterface:...>
|
@@ -49,3 +52,13 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
49
52
|
## Contributing
|
50
53
|
|
51
54
|
Bug reports and pull requests are welcome on GitHub at https://github.com/misson20000/ruby-twib.
|
55
|
+
|
56
|
+
## License
|
57
|
+
|
58
|
+
```
|
59
|
+
Copyright 2018 misson20000
|
60
|
+
|
61
|
+
Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
|
62
|
+
|
63
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
64
|
+
```
|
data/lib/twib.rb
CHANGED
@@ -8,16 +8,26 @@ require "twib/interfaces/ITwibDeviceInterface"
|
|
8
8
|
module Twib
|
9
9
|
class Error < RuntimeError
|
10
10
|
end
|
11
|
-
|
11
|
+
|
12
|
+
# An error originating from either twibd or a remote device.
|
12
13
|
class ResultError < Error
|
13
14
|
def initialize(code)
|
14
|
-
super("0x#{code.to_s(16)}")
|
15
|
-
@code = code
|
15
|
+
super("0x#{code.to_i.to_s(16)}")
|
16
|
+
@code = code.to_i
|
16
17
|
end
|
18
|
+
|
19
|
+
# @return [Integer] the bad result code that caused this error
|
17
20
|
attr_reader :code
|
18
21
|
end
|
19
|
-
|
22
|
+
|
23
|
+
# A response from either twibd or a remote device.
|
20
24
|
class Response
|
25
|
+
# @param device_id [Integer] ID of the device that this response originated from.
|
26
|
+
# @param object_id [Integer] ID of the bridge object that this response originated from.
|
27
|
+
# @param result_code [Integer] Result code
|
28
|
+
# @param tag [Integer] Tag corresponding to the request that prompted this response.
|
29
|
+
# @param payload [String] Raw data associated with the response
|
30
|
+
# @param object_ids [Array<Integer>] Object IDs sent with the response
|
21
31
|
def initialize(device_id, object_id, result_code, tag, payload, object_ids)
|
22
32
|
@device_id = device_id
|
23
33
|
@object_id = object_id
|
@@ -28,7 +38,10 @@ module Twib
|
|
28
38
|
end
|
29
39
|
|
30
40
|
attr_reader :device_id, :object_id, :result_code, :tag, :payload, :object_ids
|
31
|
-
|
41
|
+
|
42
|
+
# Raises a {ResultError} if the {#result_code} is not OK.
|
43
|
+
# @raise [ResultError]
|
44
|
+
# @return [self]
|
32
45
|
def assert_ok
|
33
46
|
if @result_code != 0 then
|
34
47
|
raise ResultError.new(@result_code)
|
@@ -36,7 +49,8 @@ module Twib
|
|
36
49
|
return self
|
37
50
|
end
|
38
51
|
end
|
39
|
-
|
52
|
+
|
53
|
+
# A request to be sent to a remote device.
|
40
54
|
class ActiveRequest
|
41
55
|
def initialize(tc, device_id, object_id, command_id, tag, payload, &block)
|
42
56
|
@tc = tc
|
@@ -50,6 +64,7 @@ module Twib
|
|
50
64
|
@response = nil
|
51
65
|
end
|
52
66
|
|
67
|
+
# @api private
|
53
68
|
def respond(response) # expects to be synchronized by @tc.mutex
|
54
69
|
@response = response
|
55
70
|
@condvar.broadcast
|
@@ -58,6 +73,8 @@ module Twib
|
|
58
73
|
end
|
59
74
|
end
|
60
75
|
|
76
|
+
# Waits for this request to receive a response.
|
77
|
+
# @return [Response]
|
61
78
|
def wait
|
62
79
|
@tc.mutex.synchronize do
|
63
80
|
while @response == nil do
|
@@ -67,16 +84,24 @@ module Twib
|
|
67
84
|
return @response
|
68
85
|
end
|
69
86
|
|
87
|
+
# Waits for this request to receive a response, and raises if the response is not OK.
|
88
|
+
# @raise [ResultError]
|
89
|
+
# @return [Response]
|
70
90
|
def wait_ok
|
71
91
|
wait.assert_ok
|
72
92
|
end
|
73
93
|
end
|
74
|
-
|
94
|
+
|
95
|
+
# A connection to twibd.
|
75
96
|
class TwibConnection
|
97
|
+
# Connects to twibd using the standard UNIX socket address.
|
98
|
+
# @return [TwibConnection]
|
76
99
|
def self.connect_unix
|
77
100
|
return self.new(UNIXSocket.new("/var/run/twibd.sock"))
|
78
101
|
end
|
79
|
-
|
102
|
+
|
103
|
+
# Creates a TwibConnection using the specified socket
|
104
|
+
# @param [BasicSocket] socket
|
80
105
|
def initialize(socket)
|
81
106
|
@socket = socket
|
82
107
|
@itmi = Interfaces::ITwibMetaInterface.new(self, 0, 0)
|
@@ -112,20 +137,29 @@ module Twib
|
|
112
137
|
@cb_thread = Thread.new do # to avoid deadlocks on I/O thread, we execute callbacks here
|
113
138
|
while @alive do
|
114
139
|
cb, response = @cb_queue.pop
|
115
|
-
cb
|
140
|
+
if cb then
|
141
|
+
cb.call(response)
|
142
|
+
end
|
116
143
|
end
|
117
144
|
end
|
118
145
|
end
|
119
146
|
|
147
|
+
# @api private
|
120
148
|
attr_reader :mutex
|
149
|
+
# @api private
|
121
150
|
attr_reader :cb_queue
|
122
|
-
|
151
|
+
|
152
|
+
# Closes the socket and stops internal threads
|
123
153
|
def close
|
124
154
|
@alive = false
|
125
155
|
@socket.close
|
126
156
|
@thread.join
|
157
|
+
@cb_queue.close
|
158
|
+
@cb_thread.join
|
127
159
|
end
|
128
160
|
|
161
|
+
# @api private
|
162
|
+
# @return [ActiveRequest]
|
129
163
|
def send(device_id, object_id, command_id, payload, &block)
|
130
164
|
tag = rand(0xffffffff)
|
131
165
|
|
@@ -140,11 +174,21 @@ module Twib
|
|
140
174
|
|
141
175
|
return rq
|
142
176
|
end
|
143
|
-
|
177
|
+
|
178
|
+
# Returns a list of devices connected to twibd.
|
179
|
+
#
|
180
|
+
# tc.list_devices
|
181
|
+
# # => [{"device_id"=>507914862, "identification"=>{...}}]
|
182
|
+
#
|
183
|
+
# Use {#open_device} to connect to one.
|
184
|
+
#
|
185
|
+
# @return [Array<Hash>]
|
144
186
|
def list_devices
|
145
187
|
@itmi.list_devices
|
146
188
|
end
|
147
189
|
|
190
|
+
# Opens a device specified by one of the device IDs returned from {#list_devices}.
|
191
|
+
# @return [Interfaces::ITwibDeviceInterface]
|
148
192
|
def open_device(device_id)
|
149
193
|
return Interfaces::ITwibDeviceInterface.new(self, device_id, 0)
|
150
194
|
end
|
data/lib/twib/interface.rb
CHANGED
@@ -1,21 +1,35 @@
|
|
1
1
|
module Twib
|
2
|
+
# Base class for Ruby bindings to a remote interface.
|
3
|
+
# An instance of this class represents a remote bridge object.
|
4
|
+
#
|
5
|
+
# This class and any subclasses should not typically be instantiated directly.
|
6
|
+
# Rather, instances should be returned from either {TwibConnection#open_device}
|
7
|
+
# or a remote command binding.
|
2
8
|
class Interface
|
9
|
+
|
10
|
+
# @param connection [TwibConnection] Twib connection to use for transport
|
11
|
+
# @param device_id [Integer] ID of the device this object exists on
|
12
|
+
# @param object_id [Integer] ID of the remote object this object is bound to
|
3
13
|
def initialize(connection, device_id, object_id)
|
4
14
|
@connection = connection
|
5
15
|
@device_id = device_id
|
6
16
|
@object_id = object_id
|
7
|
-
|
8
|
-
# object id 0 is special
|
9
|
-
#if @object_id != 0 then
|
10
|
-
# ObjectSpace.define_finalizer(self, self.class.finalize(connection, device_id, object_id))
|
11
|
-
#end
|
12
|
-
end
|
13
|
-
|
14
|
-
def self.finalize(connection, device_id, object_id)
|
15
|
-
# send close request
|
16
|
-
#connection.send_sync(device_id, object_id, 0xffffffff, String.new)
|
17
17
|
end
|
18
18
|
|
19
|
+
# Sends a request to the remote object this instance is bound to.
|
20
|
+
#
|
21
|
+
# object.send(10, [1234].pack("L<")) do |rs|
|
22
|
+
# puts "got back: " + rs.assert_ok.payload
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# object.send(10, [1234].pack("L<")).wait_ok
|
26
|
+
# # => #<Twib::TwibConnection::Response>
|
27
|
+
#
|
28
|
+
# @param command_id [Integer] ID of remtoe command to invoke
|
29
|
+
# @param payload [String] Data to send along with the request
|
30
|
+
# @yield [rs] Calls the block (in a separate thread) when a response is received, if present.
|
31
|
+
# @yieldparam rs [Response]
|
32
|
+
# @return [ActiveRequest]
|
19
33
|
def send(command_id, payload=String.new, &block)
|
20
34
|
@connection.send(@device_id, @object_id, command_id, payload, &block)
|
21
35
|
end
|
@@ -3,7 +3,9 @@ require "twib/switch/debug.rb"
|
|
3
3
|
|
4
4
|
module Twib
|
5
5
|
module Interfaces
|
6
|
+
# Debug interface bound to a specific process.
|
6
7
|
class ITwibDebugger < Interface
|
8
|
+
# @api private
|
7
9
|
module Command
|
8
10
|
QUERY_MEMORY = 10
|
9
11
|
READ_MEMORY = 11
|
@@ -17,51 +19,86 @@ module Twib
|
|
17
19
|
GET_NSO_INFOS = 19
|
18
20
|
WAIT_EVENT = 20
|
19
21
|
end
|
20
|
-
|
22
|
+
|
23
|
+
# Queries process segment information at the given address.
|
24
|
+
#
|
25
|
+
# debug.query_memory(0)
|
26
|
+
# # => {:base=>0, :size=>62308483072, :memory_type=>0,
|
27
|
+
# # :memory_attribute=>0, :permission=>0,
|
28
|
+
# # :device_ref_count=>0, :ipc_ref_count=>0}
|
29
|
+
#
|
30
|
+
# @param addr [Integer] Address to query
|
31
|
+
# @return [Hash]
|
21
32
|
def query_memory(addr)
|
22
33
|
Hash[
|
23
34
|
[:base, :size, :memory_type, :memory_attribute,
|
24
35
|
:permission, :device_ref_count, :ipc_ref_count].zip(
|
25
36
|
send(Command::QUERY_MEMORY, [addr].pack("Q<")).wait_ok.payload.unpack("Q<Q<L<L<L<L<L<"))]
|
26
37
|
end
|
27
|
-
|
38
|
+
|
39
|
+
# Reads from process memory at the given address.
|
40
|
+
# @param addr [Integer] Address to read from
|
41
|
+
# @param size [Integer] How many bytes to read
|
42
|
+
# @return [String]
|
28
43
|
def read_memory(addr, size)
|
29
44
|
send(Command::READ_MEMORY, [addr, size].pack("Q<Q<")).wait_ok.payload
|
30
45
|
end
|
31
|
-
|
46
|
+
|
47
|
+
# Writes to process memory at the given address.
|
48
|
+
# @param addr [Integer] Address to write to
|
49
|
+
# @param string [String] Data to write
|
50
|
+
# @return [String]
|
32
51
|
def write_memory(addr, string)
|
33
52
|
send(Command::WRITE_MEMORY, [addr].pack("Q<") + string).wait_ok
|
53
|
+
string
|
34
54
|
end
|
35
|
-
|
55
|
+
|
56
|
+
# Lists threads in the target process.
|
57
|
+
# @return [self]
|
36
58
|
def list_threads
|
37
59
|
raise "nyi"
|
38
60
|
end
|
39
|
-
|
61
|
+
|
62
|
+
# Gets a debug event from the target process.
|
63
|
+
# @return [Switch::Debug::Event, nil] A debug event, or nil if none were left
|
40
64
|
def get_debug_event
|
41
65
|
rs = send(Command::GET_DEBUG_EVENT).wait
|
42
66
|
if rs.result_code == 0x8c01 then # no debug events left
|
43
67
|
return nil
|
68
|
+
else
|
69
|
+
rs.assert_ok
|
44
70
|
end
|
45
71
|
return Switch::Debug::Event::Event.unpack(rs.payload)
|
46
72
|
end
|
47
|
-
|
73
|
+
|
74
|
+
# Gets a thread's context.
|
75
|
+
# @return [self]
|
48
76
|
def get_thread_context(thread_id)
|
49
77
|
raise "nyi"
|
50
78
|
end
|
51
|
-
|
79
|
+
|
80
|
+
# Breaks the target process.
|
81
|
+
# @return [self]
|
52
82
|
def break_process
|
53
83
|
raise "nyi"
|
54
84
|
end
|
55
|
-
|
85
|
+
|
86
|
+
# Continues the target process.
|
87
|
+
# @param flags [Integer] See http://www.switchbrew.org/index.php?title=SVC#ContinueDebugFlagsOld
|
88
|
+
# @return [self]
|
56
89
|
def continue_debug_event(flags, thread_ids=[])
|
57
90
|
send(Command::CONTINUE_DEBUG_EVENT, ([flags] + thread_ids).pack("L<Q<*")).wait_ok
|
58
|
-
|
91
|
+
self
|
59
92
|
end
|
60
|
-
|
93
|
+
|
94
|
+
# Sets a thread's context.
|
95
|
+
# @return [self]
|
61
96
|
def set_thread_context(thread_id)
|
62
97
|
raise "nyi"
|
63
98
|
end
|
64
|
-
|
99
|
+
|
100
|
+
# Queries NSO info for the target process.
|
101
|
+
# @return [Array<Hash>]
|
65
102
|
def get_nso_infos
|
66
103
|
response = send(Command::GET_NSO_INFOS).wait_ok.payload
|
67
104
|
count = response.unpack("Q<")[0]
|
@@ -70,13 +107,19 @@ module Twib
|
|
70
107
|
[:base, :size, :build_id].zip(response[8 + 0x30 * i, 0x30].unpack("Q<Q<a32"))]
|
71
108
|
end
|
72
109
|
end
|
73
|
-
|
110
|
+
|
111
|
+
# Yields from a separate thread when a debug event is available.
|
112
|
+
# @return [self]
|
74
113
|
def wait_event_async(&block)
|
75
114
|
send(Command::WAIT_EVENT, String.new, &block)
|
115
|
+
self
|
76
116
|
end
|
77
|
-
|
117
|
+
|
118
|
+
# Waits for a debug event to become available.
|
119
|
+
# @return [self]
|
78
120
|
def wait_event
|
79
121
|
send(Command::WAIT_EVENT).wait_ok
|
122
|
+
self
|
80
123
|
end
|
81
124
|
end
|
82
125
|
end
|
@@ -4,7 +4,9 @@ require "twib/interfaces/ITwibDebugger.rb"
|
|
4
4
|
|
5
5
|
module Twib
|
6
6
|
module Interfaces
|
7
|
+
# Main interface exposed by a device.
|
7
8
|
class ITwibDeviceInterface < Interface
|
9
|
+
# @api private
|
8
10
|
module Command
|
9
11
|
RUN = 10
|
10
12
|
REBOOT = 11
|
@@ -17,6 +19,10 @@ module Twib
|
|
17
19
|
OPEN_NAMED_PIPE = 18
|
18
20
|
OPEN_ACTIVE_DEBUGGER = 19
|
19
21
|
end
|
22
|
+
|
23
|
+
# Opens an active debugger for the process with the given PID.
|
24
|
+
# @param pid [Integer] Process ID
|
25
|
+
# @return [ITwibDebugger] Debugger interface
|
20
26
|
def open_active_debugger(pid)
|
21
27
|
ITwibDebugger.new(@connection, @device_id, send(Command::OPEN_ACTIVE_DEBUGGER, [pid].pack("Q<")).wait_ok.object_ids[0])
|
22
28
|
end
|
@@ -4,10 +4,15 @@ require "twib/interface.rb"
|
|
4
4
|
|
5
5
|
module Twib
|
6
6
|
module Interfaces
|
7
|
+
# Exposed by twibd
|
7
8
|
class ITwibMetaInterface < Interface
|
9
|
+
# @api private
|
8
10
|
module Command
|
9
11
|
LIST_DEVICES = 10
|
10
12
|
end
|
13
|
+
|
14
|
+
# Lists devices known to twibd.
|
15
|
+
# @return [Array<Hash>]
|
11
16
|
def list_devices
|
12
17
|
MessagePack.unpack(send(Command::LIST_DEVICES).wait_ok.payload)
|
13
18
|
end
|
data/lib/twib/version.rb
CHANGED
data/twib.gemspec
CHANGED
@@ -18,6 +18,8 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
+
spec.license = 'ISC'
|
22
|
+
|
21
23
|
spec.add_development_dependency "bundler", "~> 1.16"
|
22
24
|
spec.add_development_dependency "rake", "~> 10.0"
|
23
25
|
spec.add_development_dependency "rspec", "~> 3.0"
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: twib
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- misson20000
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2018-09-
|
11
|
+
date: 2018-09-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -78,6 +78,7 @@ files:
|
|
78
78
|
- ".travis.yml"
|
79
79
|
- Gemfile
|
80
80
|
- Gemfile.lock
|
81
|
+
- LICENSE
|
81
82
|
- README.md
|
82
83
|
- Rakefile
|
83
84
|
- bin/console
|
@@ -91,7 +92,8 @@ files:
|
|
91
92
|
- lib/twib/version.rb
|
92
93
|
- twib.gemspec
|
93
94
|
homepage: https://github.com/misson20000/ruby-twib/
|
94
|
-
licenses:
|
95
|
+
licenses:
|
96
|
+
- ISC
|
95
97
|
metadata: {}
|
96
98
|
post_install_message:
|
97
99
|
rdoc_options: []
|