pwntools 0.1.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/README.md +49 -0
- data/Rakefile +40 -0
- data/lib/pwn.rb +24 -0
- data/lib/pwnlib/constants/constant.rb +45 -0
- data/lib/pwnlib/constants/constants.rb +82 -0
- data/lib/pwnlib/constants/linux/amd64.rb +1558 -0
- data/lib/pwnlib/constants/linux/i386.rb +1340 -0
- data/lib/pwnlib/context.rb +220 -0
- data/lib/pwnlib/dynelf.rb +110 -0
- data/lib/pwnlib/ext/array.rb +21 -0
- data/lib/pwnlib/ext/helper.rb +21 -0
- data/lib/pwnlib/ext/integer.rb +21 -0
- data/lib/pwnlib/ext/string.rb +23 -0
- data/lib/pwnlib/memleak.rb +61 -0
- data/lib/pwnlib/pwn.rb +26 -0
- data/lib/pwnlib/reg_sort.rb +147 -0
- data/lib/pwnlib/util/cyclic.rb +120 -0
- data/lib/pwnlib/util/fiddling.rb +262 -0
- data/lib/pwnlib/util/hexdump.rb +145 -0
- data/lib/pwnlib/util/packing.rb +284 -0
- data/lib/pwnlib/version.rb +5 -0
- data/test/constants/constant_test.rb +24 -0
- data/test/constants/constants_test.rb +31 -0
- data/test/context_test.rb +131 -0
- data/test/data/victim.c +8 -0
- data/test/data/victim32 +0 -0
- data/test/data/victim64 +0 -0
- data/test/dynelf_test.rb +48 -0
- data/test/ext_test.rb +26 -0
- data/test/files/use_pwn.rb +34 -0
- data/test/files/use_pwnlib.rb +19 -0
- data/test/full_file_test.rb +16 -0
- data/test/memleak_test.rb +72 -0
- data/test/reg_sort_test.rb +41 -0
- data/test/test_helper.rb +13 -0
- data/test/util/cyclic_test.rb +36 -0
- data/test/util/fiddling_test.rb +106 -0
- data/test/util/hexdump_test.rb +179 -0
- data/test/util/packing_test.rb +168 -0
- metadata +231 -0
@@ -0,0 +1,145 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
require 'rainbow'
|
3
|
+
|
4
|
+
module Pwnlib
|
5
|
+
module Util
|
6
|
+
# Method for output a pretty hexdump.
|
7
|
+
# Since this may be used in log module, to avoid cyclic dependency, it is put in a separate module as {Fiddling}
|
8
|
+
# See {ClassMethods} for method details.
|
9
|
+
# @todo Control coloring by context?
|
10
|
+
# @example Call by specifying full module path.
|
11
|
+
# require 'pwnlib/util/hexdump'
|
12
|
+
# Pwnlib::Util::HexDump.hexdump('217')
|
13
|
+
# #=> "00000000 32 31 37 │217│\n00000003"
|
14
|
+
# @example require 'pwn' and have all methods.
|
15
|
+
# require 'pwn'
|
16
|
+
# hexdump('217')
|
17
|
+
# #=> "00000000 32 31 37 │217│\n00000003"
|
18
|
+
module HexDump
|
19
|
+
# @note Do not create and call instance method here. Instead, call module method on {HexDump}.
|
20
|
+
module ClassMethods
|
21
|
+
MARKER = "\u2502".freeze
|
22
|
+
HIGHLIGHT_STYLE = ->(s) { Rainbow(s).bg(:red) }
|
23
|
+
DEFAULT_STYLE = {
|
24
|
+
0x00 => ->(s) { Rainbow(s).red },
|
25
|
+
0x0a => ->(s) { Rainbow(s).red },
|
26
|
+
0xff => ->(s) { Rainbow(s).green },
|
27
|
+
marker: ->(s) { Rainbow(s).dimgray },
|
28
|
+
printable: ->(s) { s },
|
29
|
+
unprintable: ->(s) { Rainbow(s).dimgray }
|
30
|
+
}.freeze
|
31
|
+
|
32
|
+
# @!macro [new] hexdump_options
|
33
|
+
# Color is provided using +rainbow+ gem and only when output is a tty.
|
34
|
+
# To force enable/disable coloring, call <tt>Rainbow.enabled = true / false</tt>.
|
35
|
+
# @param [Integer] width
|
36
|
+
# The max number of characters per line.
|
37
|
+
# @param [Boolean] skip
|
38
|
+
# Whether repeated lines should be replaced by a +"*"+.
|
39
|
+
# @param [Integer] offset
|
40
|
+
# Offset of the first byte to print in the left column.
|
41
|
+
# @param [Hash{Integer, Symbol => Proc}] style
|
42
|
+
# Color scheme to use.
|
43
|
+
#
|
44
|
+
# Possible keys are:
|
45
|
+
# * <tt>0x00..0xFF</tt>, for specified byte.
|
46
|
+
# * +:marker+, for the separator in right column.
|
47
|
+
# * +:printable+, for printable bytes that don't have style specified.
|
48
|
+
# * +:unprintable+, for unprintable bytes that don't have style specified.
|
49
|
+
# The proc is called with a single argument, the string to be formatted.
|
50
|
+
# @param [String] highlight
|
51
|
+
# Convenient argument to highlight (red background) some bytes in style.
|
52
|
+
|
53
|
+
# Yields lines of a hexdump-dump of a string. Unless you have massive
|
54
|
+
# amounts of data you probably want to use {#hexdump}.
|
55
|
+
# Returns an Enumerator if no block given.
|
56
|
+
#
|
57
|
+
# @param [#read] io
|
58
|
+
# The object to be dumped.
|
59
|
+
# @!macro hexdump_options
|
60
|
+
# @return [Enumerator<String>]
|
61
|
+
# The resulting hexdump, line by line.
|
62
|
+
def hexdump_iter(io, width: 16, skip: true, offset: 0, style: {}, highlight: '')
|
63
|
+
Enumerator.new do |y|
|
64
|
+
style = DEFAULT_STYLE.merge(style)
|
65
|
+
highlight.bytes.each { |b| style[b] = HIGHLIGHT_STYLE }
|
66
|
+
(0..255).each do |b|
|
67
|
+
next if style.include?(b)
|
68
|
+
style[b] = (b.chr =~ /[[:print:]]/ ? style[:printable] : style[:unprintable])
|
69
|
+
end
|
70
|
+
|
71
|
+
styled_bytes = (0..255).map do |b|
|
72
|
+
left_hex = format('%02x', b)
|
73
|
+
c = b.chr
|
74
|
+
right_char = (c =~ /[[:print:]]/ ? c : "\u00b7")
|
75
|
+
[style[b].call(left_hex), style[b].call(right_char)]
|
76
|
+
end
|
77
|
+
|
78
|
+
marker = style[:marker].call(MARKER)
|
79
|
+
spacer = ' '
|
80
|
+
|
81
|
+
byte_index = offset
|
82
|
+
skipping = false
|
83
|
+
last_chunk = ''
|
84
|
+
|
85
|
+
loop do
|
86
|
+
# We assume that chunk is in ASCII-8BIT encoding.
|
87
|
+
chunk = io.read(width)
|
88
|
+
break unless chunk
|
89
|
+
chunk_bytes = chunk.bytes
|
90
|
+
start_byte_index = byte_index
|
91
|
+
byte_index += chunk_bytes.size
|
92
|
+
|
93
|
+
# Yield * once for repeated lines.
|
94
|
+
if skip && last_chunk == chunk
|
95
|
+
y << '*' unless skipping
|
96
|
+
skipping = true
|
97
|
+
next
|
98
|
+
end
|
99
|
+
skipping = false
|
100
|
+
last_chunk = chunk
|
101
|
+
|
102
|
+
hex_bytes = ''
|
103
|
+
printable = ''
|
104
|
+
chunk_bytes.each_with_index do |b, i|
|
105
|
+
left_hex, right_char = styled_bytes[b]
|
106
|
+
hex_bytes << left_hex
|
107
|
+
printable << right_char
|
108
|
+
if i % 4 == 3 && i != chunk_bytes.size - 1
|
109
|
+
hex_bytes << spacer
|
110
|
+
printable << marker
|
111
|
+
end
|
112
|
+
hex_bytes << ' '
|
113
|
+
end
|
114
|
+
|
115
|
+
if chunk_bytes.size < width
|
116
|
+
padded_hex_length = 3 * width + (width - 1) / 4
|
117
|
+
hex_length = 3 * chunk_bytes.size + (chunk_bytes.size - 1) / 4
|
118
|
+
hex_bytes << ' ' * (padded_hex_length - hex_length)
|
119
|
+
end
|
120
|
+
|
121
|
+
y << format("%08x %s #{MARKER}%s#{MARKER}", start_byte_index, hex_bytes, printable)
|
122
|
+
end
|
123
|
+
|
124
|
+
y << format('%08x', byte_index)
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
# Returns a hexdump-dump of a string.
|
129
|
+
#
|
130
|
+
# @param [String] str
|
131
|
+
# The string to be hexdumped.
|
132
|
+
# @!macro hexdump_options
|
133
|
+
# @return [String]
|
134
|
+
# The resulting hexdump.
|
135
|
+
def hexdump(str, width: 16, skip: true, offset: 0, style: {}, highlight: '')
|
136
|
+
hexdump_iter(StringIO.new(str),
|
137
|
+
width: width, skip: skip, offset: offset, style: style,
|
138
|
+
highlight: highlight).to_a.join("\n")
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
extend ClassMethods
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,284 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'pwnlib/context'
|
4
|
+
|
5
|
+
module Pwnlib
|
6
|
+
module Util
|
7
|
+
# Methods for integer pack/unpack.
|
8
|
+
# See {ClassMethods} for method details.
|
9
|
+
# @example Call by specifying full module path.
|
10
|
+
# require 'pwnlib/util/packing'
|
11
|
+
# Pwnlib::Util::Packing.p8(217) #=> "\xD9"
|
12
|
+
# @example require 'pwn' and have all methods.
|
13
|
+
# require 'pwn'
|
14
|
+
# p8(217) #=> "\xD9"
|
15
|
+
module Packing
|
16
|
+
# @note Do not create and call instance method here. Instead, call module method on {Packing}.
|
17
|
+
module ClassMethods
|
18
|
+
# Pack arbitrary-sized integer.
|
19
|
+
#
|
20
|
+
# +bits+ indicates number of bits that packed output should use.
|
21
|
+
# The output would be padded to be byte-aligned.
|
22
|
+
#
|
23
|
+
# +bits+ can also be the string 'all',
|
24
|
+
# indicating that the result should be long enough to hold all bits of the number.
|
25
|
+
#
|
26
|
+
# @param [Integer] number
|
27
|
+
# Number to be packed.
|
28
|
+
# @param [Integer, 'all'] bits
|
29
|
+
# Number of bits the output should have,
|
30
|
+
# or +'all'+ for all bits.
|
31
|
+
# Default to +context.bits+.
|
32
|
+
# @param [String] endian
|
33
|
+
# Endian to use when packing.
|
34
|
+
# Can be any value accepted by context (See {Context::ContextType}).
|
35
|
+
# Default to +context.endian+.
|
36
|
+
# @param [Boolean, String] signed
|
37
|
+
# Whether the input number should be considered signed when +bits+ is +'all'+.
|
38
|
+
# Can be any value accepted by context (See {Context::ContextType}).
|
39
|
+
# Default to +context.signed+.
|
40
|
+
# @return [String]
|
41
|
+
# The packed string.
|
42
|
+
# @raise [ArgumentError]
|
43
|
+
# When input integer can't be packed into the size specified by +bits+ and +signed+.
|
44
|
+
#
|
45
|
+
# @example
|
46
|
+
# pack(0x34, bits: 8) #=> '4'
|
47
|
+
# pack(0x1234, bits: 16, endian: 'little') #=> "4\x12"
|
48
|
+
# pack(0xFF, bits: 'all', signed: false) #=> "\xFF"
|
49
|
+
# pack(0xFF, bits: 'all', endian: 'big', signed: true) #=> "\x00\xFF"
|
50
|
+
def pack(number, bits: nil, endian: nil, signed: nil)
|
51
|
+
if bits == 'all'
|
52
|
+
bits = nil
|
53
|
+
is_all = true
|
54
|
+
else
|
55
|
+
is_all = false
|
56
|
+
end
|
57
|
+
|
58
|
+
context.local(bits: bits, endian: endian, signed: signed) do
|
59
|
+
bits = context.bits
|
60
|
+
endian = context.endian
|
61
|
+
signed = context.signed
|
62
|
+
|
63
|
+
# Verify that bits make sense
|
64
|
+
if signed
|
65
|
+
bits = (number.bit_length | 7) + 1 if is_all
|
66
|
+
|
67
|
+
limit = 1 << (bits - 1)
|
68
|
+
unless -limit <= number && number < limit
|
69
|
+
raise ArgumentError, "signed number=#{number} does not fit within bits=#{bits}"
|
70
|
+
end
|
71
|
+
else
|
72
|
+
if is_all
|
73
|
+
if number < 0
|
74
|
+
raise ArgumentError, "Can't pack negative number with bits='all' and signed=false"
|
75
|
+
end
|
76
|
+
bits = number.zero? ? 8 : ((number.bit_length - 1) | 7) + 1
|
77
|
+
end
|
78
|
+
|
79
|
+
limit = 1 << bits
|
80
|
+
unless 0 <= number && number < limit
|
81
|
+
raise ArgumentError, "unsigned number=#{number} does not fit within bits=#{bits}"
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
number &= (1 << bits) - 1
|
86
|
+
bytes = (bits + 7) / 8
|
87
|
+
|
88
|
+
out = []
|
89
|
+
bytes.times do
|
90
|
+
out << (number & 0xFF)
|
91
|
+
number >>= 8
|
92
|
+
end
|
93
|
+
out = out.pack('C*')
|
94
|
+
|
95
|
+
endian == 'little' ? out : out.reverse
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
# Unpack packed string back to integer.
|
100
|
+
#
|
101
|
+
# +bits+ indicates number of bits that should be used from input data.
|
102
|
+
#
|
103
|
+
# +bits+ can also be the string +'all'+,
|
104
|
+
# indicating that all bytes from input should be used.
|
105
|
+
#
|
106
|
+
# @param [String] data
|
107
|
+
# String to be unpacked.
|
108
|
+
# @param [Integer, 'all'] bits
|
109
|
+
# Number of bits to be used from +data+,
|
110
|
+
# or +'all'+ for all bits.
|
111
|
+
# Default to +context.bits+
|
112
|
+
# @param [String] endian
|
113
|
+
# Endian to use when unpacking.
|
114
|
+
# Can be any value accepted by context (See {Context::ContextType}).
|
115
|
+
# Default to +context.endian+.
|
116
|
+
# @param [Boolean, String] signed
|
117
|
+
# Whether the output number should be signed.
|
118
|
+
# Can be any value accepted by context (See {Context::ContextType}).
|
119
|
+
# Default to +context.signed+.
|
120
|
+
# @return [Integer]
|
121
|
+
# The unpacked number.
|
122
|
+
# @raise [ArgumentError]
|
123
|
+
# When +data.size+ doesn't match +bits+.
|
124
|
+
#
|
125
|
+
# @example
|
126
|
+
# unpack('4', bits: 8) #=> 52
|
127
|
+
# unpack("\x3F", bits: 6, signed: false) #=> 63
|
128
|
+
# unpack("\x3F", bits: 6, signed: true) #=> -1
|
129
|
+
def unpack(data, bits: nil, endian: nil, signed: nil)
|
130
|
+
bits = data.size * 8 if bits == 'all'
|
131
|
+
|
132
|
+
context.local(bits: bits, endian: endian, signed: signed) do
|
133
|
+
bits = context.bits
|
134
|
+
endian = context.endian
|
135
|
+
signed = context.signed
|
136
|
+
bytes = (bits + 7) / 8
|
137
|
+
|
138
|
+
unless data.size == bytes
|
139
|
+
raise ArgumentError, "data.size=#{data.size} does not match with bits=#{bits}"
|
140
|
+
end
|
141
|
+
|
142
|
+
data = data.reverse if endian == 'little'
|
143
|
+
data = data.unpack('C*')
|
144
|
+
number = 0
|
145
|
+
data.each { |c| number = (number << 8) + c }
|
146
|
+
number &= (1 << bits) - 1
|
147
|
+
if signed
|
148
|
+
signbit = number & (1 << (bits - 1))
|
149
|
+
number -= 2 * signbit
|
150
|
+
end
|
151
|
+
number
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
# Split the data into chunks, and unpack each element.
|
156
|
+
#
|
157
|
+
# +bits+ indicates how many bits each chunk should be.
|
158
|
+
# This should be a multiple of 8,
|
159
|
+
# and size of +data+ should be divisible by +bits / 8+.
|
160
|
+
#
|
161
|
+
# +bits+ can also be the string +'all'+,
|
162
|
+
# indicating that all bytes from input would be used,
|
163
|
+
# and result would be an array with one element.
|
164
|
+
#
|
165
|
+
# @param [String] data
|
166
|
+
# String to be unpacked.
|
167
|
+
# @param [Integer, 'all'] bits
|
168
|
+
# Number of bits to be used for each chunk of +data+,
|
169
|
+
# or +'all'+ for all bits.
|
170
|
+
# Default to +context.bits+
|
171
|
+
# @param [String] endian
|
172
|
+
# Endian to use when unpacking.
|
173
|
+
# Can be any value accepted by context (See {Context::ContextType}).
|
174
|
+
# Default to +context.endian+.
|
175
|
+
# @param [Boolean, String] signed
|
176
|
+
# Whether the output number should be signed.
|
177
|
+
# Can be any value accepted by context (See {Context::ContextType}).
|
178
|
+
# Default to +context.signed+.
|
179
|
+
# @return [Array<Integer>]
|
180
|
+
# The unpacked numbers.
|
181
|
+
# @raise [ArgumentError]
|
182
|
+
# When +bits+ isn't divisible by 8 or +data.size+ isn't divisible by +bits / 8+.
|
183
|
+
#
|
184
|
+
# @todo
|
185
|
+
# Support +bits+ not divisible by 8, if ever found this useful.
|
186
|
+
#
|
187
|
+
# @example
|
188
|
+
# unpack_many('haha', bits: 8) #=> [104, 97, 104, 97]
|
189
|
+
# unpack_many("\xFF\x01\x02\xFE", bits: 16, endian: 'little', signed: true) #=> [511, -510]
|
190
|
+
# unpack_many("\xFF\x01\x02\xFE", bits: 16, endian: 'big', signed: false) #=> [65281, 766]
|
191
|
+
def unpack_many(data, bits: nil, endian: nil, signed: nil)
|
192
|
+
return [unpack(data, bits: bits, endian: endian, signed: signed)] if bits == 'all'
|
193
|
+
|
194
|
+
context.local(bits: bits, endian: endian, signed: signed) do
|
195
|
+
bits = context.bits
|
196
|
+
|
197
|
+
# TODO(Darkpi): Support this if found useful.
|
198
|
+
raise ArgumentError, 'bits must be a multiple of 8' if bits % 8 != 0
|
199
|
+
|
200
|
+
bytes = bits / 8
|
201
|
+
|
202
|
+
if data.size % bytes != 0
|
203
|
+
raise ArgumentError, "data.size=#{data.size} must be a multiple of bytes=#{bytes}"
|
204
|
+
end
|
205
|
+
ret = []
|
206
|
+
(data.size / bytes).times do |idx|
|
207
|
+
x1 = idx * bytes
|
208
|
+
x2 = x1 + bytes
|
209
|
+
# We already set local context, no need to pass things down.
|
210
|
+
ret << unpack(data[x1...x2], bits: bits)
|
211
|
+
end
|
212
|
+
ret
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
{ 8 => 'c', 16 => 's', 32 => 'l', 64 => 'q' }.each do |sz, ch|
|
217
|
+
define_method("p#{sz}") do |num, **kwargs|
|
218
|
+
context.local(**kwargs) do
|
219
|
+
c = context.signed ? ch : ch.upcase
|
220
|
+
arrow = context.endian == 'little' ? '<' : '>'
|
221
|
+
arrow = '' if sz == 8
|
222
|
+
[num].pack("#{c}#{arrow}")
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
define_method("u#{sz}") do |data, **kwargs|
|
227
|
+
context.local(**kwargs) do
|
228
|
+
c = context.signed ? ch : ch.upcase
|
229
|
+
arrow = context.endian == 'little' ? '<' : '>'
|
230
|
+
arrow = '' if sz == 8
|
231
|
+
data.unpack("#{c}#{arrow}")[0]
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
|
236
|
+
# TODO(Darkpi): pwntools-python have this for performance reason,
|
237
|
+
# but current implementation doesn't offer that much performance
|
238
|
+
# relative to what pwntools-python do. Maybe we should initialize
|
239
|
+
# those functions (p8lu, ...) like in pwntools-python?
|
240
|
+
[%w(pack p), %w(unpack u)].each do |v1, v2|
|
241
|
+
define_method("make_#{v1}er") do |bits: nil, endian: nil, signed: nil|
|
242
|
+
context.local(bits: bits, endian: endian, signed: signed) do
|
243
|
+
bits = context.bits
|
244
|
+
endian = context.endian
|
245
|
+
signed = context.signed
|
246
|
+
|
247
|
+
if [8, 16, 32, 64].include?(bits)
|
248
|
+
->(num) { public_send("#{v2}#{bits}", num, endian: endian, signed: signed) }
|
249
|
+
else
|
250
|
+
->(num) { public_send(v1, num, bits: bits, endian: endian, signed: signed) }
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
def flat(*args, **kwargs, &preprocessor)
|
257
|
+
ret = []
|
258
|
+
p = make_packer(**kwargs)
|
259
|
+
args.each do |it|
|
260
|
+
if preprocessor && !it.is_a?(Array)
|
261
|
+
r = preprocessor[it]
|
262
|
+
it = r unless r.nil?
|
263
|
+
end
|
264
|
+
v = case it
|
265
|
+
when Array then flat(*it, **kwargs, &preprocessor)
|
266
|
+
when Integer then p[it]
|
267
|
+
when String then it.force_encoding('ASCII-8BIT')
|
268
|
+
else
|
269
|
+
raise ArgumentError, "flat does not support values of type #{it.class}"
|
270
|
+
end
|
271
|
+
ret << v
|
272
|
+
end
|
273
|
+
ret.join
|
274
|
+
end
|
275
|
+
|
276
|
+
# TODO(Darkpi): fit! Which seems super useful.
|
277
|
+
|
278
|
+
include ::Pwnlib::Context
|
279
|
+
end
|
280
|
+
|
281
|
+
extend ClassMethods
|
282
|
+
end
|
283
|
+
end
|
284
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# encoding: ASCII-8BIT
|
2
|
+
|
3
|
+
require 'test_helper'
|
4
|
+
require 'pwnlib/constants/constant'
|
5
|
+
|
6
|
+
class ConstantTest < MiniTest::Test
|
7
|
+
Constant = ::Pwnlib::Constants::Constant
|
8
|
+
|
9
|
+
def test_methods
|
10
|
+
a1 = Constant.new('a', 1)
|
11
|
+
assert_equal(1, a1.to_i)
|
12
|
+
assert_equal(3, a1 | 2)
|
13
|
+
assert_operator(a1, :==, 1)
|
14
|
+
# test coerce
|
15
|
+
assert_operator(1, :==, a1)
|
16
|
+
assert_operator(a1, :==, Constant.new('b', 1))
|
17
|
+
refute_operator(a1, :==, Constant.new('a', 3))
|
18
|
+
assert_equal(3, a1 | Constant.new('a', 2))
|
19
|
+
assert_equal(3, 2 | a1)
|
20
|
+
assert_equal(1, 2 - a1)
|
21
|
+
|
22
|
+
assert_equal(a1.method(:chr).call, "\x01")
|
23
|
+
end
|
24
|
+
end
|