pedump 0.5.3
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/Gemfile +16 -0
- data/Gemfile.lock +90 -0
- data/LICENSE.txt +20 -0
- data/README.md +410 -0
- data/Rakefile +179 -0
- data/VERSION +1 -0
- data/bin/pedump +7 -0
- data/data/fs.txt +224 -0
- data/data/jc-userdb.txt +14371 -0
- data/data/sig.bin +0 -0
- data/data/signatures.txt +678 -0
- data/data/userdb.txt +14083 -0
- data/lib/pedump.rb +868 -0
- data/lib/pedump/cli.rb +804 -0
- data/lib/pedump/comparer.rb +147 -0
- data/lib/pedump/composite_io.rb +56 -0
- data/lib/pedump/core.rb +38 -0
- data/lib/pedump/core_ext/try.rb +57 -0
- data/lib/pedump/loader.rb +393 -0
- data/lib/pedump/loader/minidump.rb +351 -0
- data/lib/pedump/loader/section.rb +57 -0
- data/lib/pedump/logger.rb +67 -0
- data/lib/pedump/ne.rb +425 -0
- data/lib/pedump/ne/version_info.rb +171 -0
- data/lib/pedump/packer.rb +173 -0
- data/lib/pedump/pe.rb +121 -0
- data/lib/pedump/resources.rb +436 -0
- data/lib/pedump/security.rb +58 -0
- data/lib/pedump/sig_parser.rb +507 -0
- data/lib/pedump/tls.rb +17 -0
- data/lib/pedump/unpacker.rb +26 -0
- data/lib/pedump/unpacker/aspack.rb +858 -0
- data/lib/pedump/unpacker/upx.rb +13 -0
- data/lib/pedump/version.rb +10 -0
- data/lib/pedump/version_info.rb +171 -0
- data/misc/aspack/Makefile +3 -0
- data/misc/aspack/aspack_unlzx.c +92 -0
- data/misc/aspack/lzxdec.c +479 -0
- data/misc/aspack/lzxdec.h +56 -0
- data/misc/nedump.c +751 -0
- data/pedump.gemspec +109 -0
- metadata +227 -0
@@ -0,0 +1,351 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'iostruct'
|
4
|
+
|
5
|
+
class PEdump
|
6
|
+
|
7
|
+
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms680378(v=vs.85).aspx
|
8
|
+
class MINIDUMP_HEADER < IOStruct.new 'a4LLLLLQ',
|
9
|
+
:Signature,
|
10
|
+
:Version,
|
11
|
+
:NumberOfStreams,
|
12
|
+
:StreamDirectoryRva,
|
13
|
+
:CheckSum,
|
14
|
+
:TimeDateStamp,
|
15
|
+
:Flags
|
16
|
+
|
17
|
+
def valid?
|
18
|
+
self.Signature == 'MDMP'
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
MINIDUMP_LOCATION_DESCRIPTOR = IOStruct.new 'LL', :DataSize, :Rva
|
23
|
+
|
24
|
+
class MINIDUMP_DIRECTORY < IOStruct.new 'L', :StreamType, :Location
|
25
|
+
def self.read io
|
26
|
+
r = super
|
27
|
+
r.Location = MINIDUMP_LOCATION_DESCRIPTOR.read(io)
|
28
|
+
r
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
MINIDUMP_MEMORY_INFO = IOStruct.new 'QQLLQLLLL',
|
33
|
+
:BaseAddress,
|
34
|
+
:AllocationBase,
|
35
|
+
:AllocationProtect,
|
36
|
+
:__alignment1,
|
37
|
+
:RegionSize,
|
38
|
+
:State,
|
39
|
+
:Protect,
|
40
|
+
:Type,
|
41
|
+
:__alignment2
|
42
|
+
|
43
|
+
class MINIDUMP_MEMORY_INFO_LIST < IOStruct.new 'LLQ',
|
44
|
+
:SizeOfHeader,
|
45
|
+
:SizeOfEntry,
|
46
|
+
:NumberOfEntries,
|
47
|
+
:entries
|
48
|
+
|
49
|
+
def self.read io
|
50
|
+
r = super
|
51
|
+
r.entries = r.NumberOfEntries.times.map{ MINIDUMP_MEMORY_INFO.read(io) }
|
52
|
+
r
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
MINIDUMP_MEMORY_DESCRIPTOR = IOStruct.new 'QLL',
|
57
|
+
:StartOfMemoryRange,
|
58
|
+
:DataSize,
|
59
|
+
:Rva
|
60
|
+
|
61
|
+
class MINIDUMP_MEMORY_LIST < IOStruct.new 'L',
|
62
|
+
:NumberOfMemoryRanges,
|
63
|
+
:MemoryRanges
|
64
|
+
|
65
|
+
def self.read io
|
66
|
+
r = super
|
67
|
+
r.MemoryRanges = r.NumberOfMemoryRanges.times.map{ MINIDUMP_MEMORY_DESCRIPTOR.read(io) }
|
68
|
+
r
|
69
|
+
end
|
70
|
+
|
71
|
+
def entries; self.MemoryRanges; end
|
72
|
+
end
|
73
|
+
|
74
|
+
MINIDUMP_MEMORY_DESCRIPTOR64 = IOStruct.new 'QQ',
|
75
|
+
:StartOfMemoryRange,
|
76
|
+
:DataSize
|
77
|
+
|
78
|
+
class MINIDUMP_MEMORY64_LIST < IOStruct.new 'QQ',
|
79
|
+
:NumberOfMemoryRanges,
|
80
|
+
:BaseRva,
|
81
|
+
:MemoryRanges
|
82
|
+
|
83
|
+
def self.read io
|
84
|
+
r = super
|
85
|
+
r.MemoryRanges = r.NumberOfMemoryRanges.times.map{ MINIDUMP_MEMORY_DESCRIPTOR64.read(io) }
|
86
|
+
r
|
87
|
+
end
|
88
|
+
|
89
|
+
def entries; self.MemoryRanges; end
|
90
|
+
end
|
91
|
+
|
92
|
+
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms680394(v=vs.85).aspx
|
93
|
+
MINIDUMP_STREAM_TYPE = {
|
94
|
+
0 => :UnusedStream,
|
95
|
+
1 => :ReservedStream0,
|
96
|
+
2 => :ReservedStream1,
|
97
|
+
3 => :ThreadListStream,
|
98
|
+
4 => :ModuleListStream,
|
99
|
+
5 => :MemoryListStream, # MINIDUMP_MEMORY_LIST
|
100
|
+
6 => :ExceptionStream,
|
101
|
+
7 => :SystemInfoStream,
|
102
|
+
8 => :ThreadExListStream,
|
103
|
+
9 => :Memory64ListStream, # MINIDUMP_MEMORY64_LIST
|
104
|
+
10 => :CommentStreamA,
|
105
|
+
11 => :CommentStreamW,
|
106
|
+
12 => :HandleDataStream,
|
107
|
+
13 => :FunctionTableStream,
|
108
|
+
14 => :UnloadedModuleListStream,
|
109
|
+
15 => :MiscInfoStream,
|
110
|
+
16 => :MemoryInfoListStream, # MINIDUMP_MEMORY_INFO_LIST
|
111
|
+
17 => :ThreadInfoListStream,
|
112
|
+
18 => :HandleOperationListStream,
|
113
|
+
0xffff => :LastReservedStream,
|
114
|
+
|
115
|
+
# Special types saved by google breakpad
|
116
|
+
# https://chromium.googlesource.com/breakpad/breakpad/+/846b6335c5b0ba46dfa2ed96fccfa3f7a02fa2f1/src/google_breakpad/common/minidump_format.h#311
|
117
|
+
0x47670001 => :BreakpadInfoStream,
|
118
|
+
0x47670002 => :BreakpadAssertionInfoStream,
|
119
|
+
0x47670003 => :BreakpadLinuxCpuInfo,
|
120
|
+
0x47670004 => :BreakpadLinuxProcStatus,
|
121
|
+
0x47670005 => :BreakpadLinuxLsbRelease,
|
122
|
+
0x47670006 => :BreakpadLinuxCmdLine,
|
123
|
+
0x47670007 => :BreakpadLinuxEnviron,
|
124
|
+
0x47670008 => :BreakpadLinuxAuxv,
|
125
|
+
0x47670009 => :BreakpadLinuxMaps,
|
126
|
+
0x4767000A => :BreakpadLinuxDsoDebug,
|
127
|
+
|
128
|
+
# Saved by crashpad
|
129
|
+
# https://chromium.googlesource.com/crashpad/crashpad/+/doc/minidump/minidump_extensions.h#95
|
130
|
+
0x43500001 => :CrashpadInfo,
|
131
|
+
|
132
|
+
# Saved by Syzyasan
|
133
|
+
# https://github.com/google/syzygy/blob/c8bb4927f07fec0de8834c4774ddaafef0bc099f/syzygy/kasko/api/client.h#L28
|
134
|
+
# https://github.com/google/syzygy/blob/master/syzygy/crashdata/crashdata.proto
|
135
|
+
0x4B6B0001 => :SyzyasanCrashdata,
|
136
|
+
|
137
|
+
# Saved by Chromium
|
138
|
+
0x4B6B0002 => :ChromiumStabilityReport,
|
139
|
+
0x4B6B0003 => :ChromiumSystemProfile,
|
140
|
+
0x4B6B0004 => :ChromiumGwpAsanData,
|
141
|
+
}
|
142
|
+
|
143
|
+
class Loader
|
144
|
+
class Minidump
|
145
|
+
attr_accessor :hdr, :streams, :io
|
146
|
+
|
147
|
+
def initialize io
|
148
|
+
@io = io
|
149
|
+
@hdr = MINIDUMP_HEADER.read(@io)
|
150
|
+
raise "invalid minidump" unless @hdr.valid?
|
151
|
+
end
|
152
|
+
|
153
|
+
def streams
|
154
|
+
@streams ||=
|
155
|
+
begin
|
156
|
+
@io.seek(@hdr.StreamDirectoryRva)
|
157
|
+
@hdr.NumberOfStreams.times.map do
|
158
|
+
dir = MINIDUMP_DIRECTORY.read(io)
|
159
|
+
dir.Location.empty? ? nil : dir
|
160
|
+
end.compact
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
164
|
+
def stream_by_name(name)
|
165
|
+
type = MINIDUMP_STREAM_TYPE.invert[name]
|
166
|
+
raise "Unknown type symbol #{name}!" if !type
|
167
|
+
|
168
|
+
streams.find { |s| s.StreamType == type }
|
169
|
+
end
|
170
|
+
|
171
|
+
def memory_info_list
|
172
|
+
# MINIDUMP_MEMORY_INFO_LIST
|
173
|
+
stream = stream_by_name(:MemoryInfoListStream)
|
174
|
+
return nil unless stream
|
175
|
+
io.seek stream.Location.Rva
|
176
|
+
MINIDUMP_MEMORY_INFO_LIST.read io
|
177
|
+
end
|
178
|
+
|
179
|
+
def memory_list
|
180
|
+
# MINIDUMP_MEMORY_LIST
|
181
|
+
stream = stream_by_name(:MemoryListStream)
|
182
|
+
return nil unless stream
|
183
|
+
io.seek stream.Location.Rva
|
184
|
+
MINIDUMP_MEMORY_LIST.read io
|
185
|
+
end
|
186
|
+
|
187
|
+
def memory64_list
|
188
|
+
# MINIDUMP_MEMORY64_LIST
|
189
|
+
stream = stream_by_name(:Memory64ListStream)
|
190
|
+
return nil unless stream
|
191
|
+
io.seek stream.Location.Rva
|
192
|
+
MINIDUMP_MEMORY64_LIST.read io
|
193
|
+
end
|
194
|
+
|
195
|
+
MemoryRange = Struct.new :file_offset, :va, :size
|
196
|
+
|
197
|
+
# set options[:merge] = true to merge adjacent memory ranges
|
198
|
+
def memory_ranges options = {}
|
199
|
+
if memory64_list
|
200
|
+
ml = memory64_list
|
201
|
+
file_offset = ml.BaseRva
|
202
|
+
r = []
|
203
|
+
if options[:merge]
|
204
|
+
ml.entries.each do |x|
|
205
|
+
if r.last && r.last.va + r.last.size == x.StartOfMemoryRange
|
206
|
+
# if section VA == prev_section.VA + prev_section.SIZE
|
207
|
+
# then just increase the size of previous section
|
208
|
+
r.last.size += x.DataSize
|
209
|
+
else
|
210
|
+
r << MemoryRange.new( file_offset, x.StartOfMemoryRange, x.DataSize )
|
211
|
+
end
|
212
|
+
file_offset += x.DataSize
|
213
|
+
end
|
214
|
+
else
|
215
|
+
ml.entries.each do |x|
|
216
|
+
r << MemoryRange.new( file_offset, x.StartOfMemoryRange, x.DataSize )
|
217
|
+
file_offset += x.DataSize
|
218
|
+
end
|
219
|
+
end
|
220
|
+
return r
|
221
|
+
elsif memory_list
|
222
|
+
ml = memory_list
|
223
|
+
r = []
|
224
|
+
if options[:merge]
|
225
|
+
ml.entries.each do |x|
|
226
|
+
if r.last && r.last.va + r.last.size == x.StartOfMemoryRange
|
227
|
+
# if section VA == prev_section.VA + prev_section.SIZE
|
228
|
+
# then just increase the size of previous section
|
229
|
+
r.last.size += x.DataSize
|
230
|
+
else
|
231
|
+
r << MemoryRange.new( x.Rva, x.StartOfMemoryRange, x.DataSize )
|
232
|
+
end
|
233
|
+
end
|
234
|
+
else
|
235
|
+
ml.entries.each do |x|
|
236
|
+
r << MemoryRange.new( x.Rva, x.StartOfMemoryRange, x.DataSize )
|
237
|
+
end
|
238
|
+
end
|
239
|
+
return r
|
240
|
+
else
|
241
|
+
raise "Could not find memory ranges"
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
end # class Minidump
|
246
|
+
end # class Loader
|
247
|
+
end # module PEdump
|
248
|
+
|
249
|
+
##############################################
|
250
|
+
|
251
|
+
if $0 == __FILE__
|
252
|
+
require 'pp'
|
253
|
+
require 'optparse'
|
254
|
+
|
255
|
+
options = {}
|
256
|
+
opt_parse = OptionParser.new do |opts|
|
257
|
+
opts.banner = "Usage: #{$0} [options] <minidump>"
|
258
|
+
|
259
|
+
opts.on("--all", "Print all of the following sections") do
|
260
|
+
options[:all] = true
|
261
|
+
end
|
262
|
+
opts.on("--header", "Print minidump header") do
|
263
|
+
options[:header] = true
|
264
|
+
end
|
265
|
+
opts.on("--streams", "Print out the streams present") do
|
266
|
+
options[:streams] = true
|
267
|
+
end
|
268
|
+
opts.on("--memory-ranges", "Print out memory ranges included in the minidump") do
|
269
|
+
options[:memory_ranges] = true
|
270
|
+
end
|
271
|
+
opts.on("--breakpad", "Print out breakpad text sections if present") do
|
272
|
+
options[:breakpad] = true
|
273
|
+
end
|
274
|
+
opts.separator ''
|
275
|
+
|
276
|
+
opts.on("--memory <address>", "Print the memory range beginning at address") do |m|
|
277
|
+
options[:memory] = m.hex
|
278
|
+
end
|
279
|
+
opts.separator ''
|
280
|
+
|
281
|
+
opts.on("-h", "--help", "Help") do
|
282
|
+
puts opts
|
283
|
+
exit 0
|
284
|
+
end
|
285
|
+
end
|
286
|
+
|
287
|
+
opt_parse.parse!
|
288
|
+
|
289
|
+
if ARGV.empty?
|
290
|
+
$stderr.puts opt_parse.help
|
291
|
+
exit 1
|
292
|
+
end
|
293
|
+
|
294
|
+
io = open(ARGV.first, "rb")
|
295
|
+
md = PEdump::Loader::Minidump.new io
|
296
|
+
|
297
|
+
if options[:all] || options[:header]
|
298
|
+
pp md.hdr
|
299
|
+
puts
|
300
|
+
end
|
301
|
+
|
302
|
+
if options[:all] || options[:streams]
|
303
|
+
puts "[.] Streams present in the minidump:"
|
304
|
+
md.streams.each do |s|
|
305
|
+
if PEdump::MINIDUMP_STREAM_TYPE[s.StreamType]
|
306
|
+
puts "[.] #{PEdump::MINIDUMP_STREAM_TYPE[s.StreamType]}"
|
307
|
+
else
|
308
|
+
puts "[.] Unknown stream type #{s.StreamType}"
|
309
|
+
end
|
310
|
+
end
|
311
|
+
puts
|
312
|
+
end
|
313
|
+
|
314
|
+
if options[:all] || options[:breakpad]
|
315
|
+
[ :BreakpadLinuxCpuInfo, :BreakpadLinuxProcStatus, :BreakpadLinuxMaps,
|
316
|
+
:BreakpadLinuxCmdLine, :BreakpadLinuxEnviron ].each { |name|
|
317
|
+
stream = md.stream_by_name(name)
|
318
|
+
next if !stream
|
319
|
+
|
320
|
+
io.seek stream.Location.Rva
|
321
|
+
contents = io.read(stream.Location.DataSize)
|
322
|
+
|
323
|
+
if contents !~ /[^[:print:][:space:]]/
|
324
|
+
puts "[.] Section #{name}:"
|
325
|
+
puts contents
|
326
|
+
else
|
327
|
+
puts "[.] Section #{name}: #{contents.inspect}"
|
328
|
+
end
|
329
|
+
puts
|
330
|
+
}
|
331
|
+
end
|
332
|
+
|
333
|
+
if options[:all] || options[:memory_ranges]
|
334
|
+
puts "[.] #{md.memory_ranges.size} memory ranges"
|
335
|
+
puts "[.] #{md.memory_ranges(:merge => true).size} merged memory ranges"
|
336
|
+
puts
|
337
|
+
|
338
|
+
printf "[.] %16s %8s\n", "addr", "size"
|
339
|
+
md.memory_ranges(:merge => true).sort_by { |mr| mr.va }.each do |mr|
|
340
|
+
printf "[.] %16x %8x\n", mr.va, mr.size
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
if options[:memory]
|
345
|
+
mr = md.memory_ranges(:merge => true).find { |r| r.va == options[:memory] }
|
346
|
+
raise "Could not find the specified region" if !mr
|
347
|
+
|
348
|
+
io.seek(mr.file_offset)
|
349
|
+
print io.read(mr.size)
|
350
|
+
end
|
351
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
class PEdump::Loader
|
2
|
+
class Section
|
3
|
+
attr_accessor :hdr
|
4
|
+
attr_writer :data
|
5
|
+
|
6
|
+
EMPTY_DATA = ''.force_encoding('binary')
|
7
|
+
|
8
|
+
def initialize x = nil, args = {}
|
9
|
+
if x.is_a?(PEdump::IMAGE_SECTION_HEADER)
|
10
|
+
@hdr = x.dup
|
11
|
+
end
|
12
|
+
@data = EMPTY_DATA.dup
|
13
|
+
@deferred_load_io = args[:deferred_load_io]
|
14
|
+
@deferred_load_pos = args[:deferred_load_pos] || (@hdr && @hdr.PointerToRawData)
|
15
|
+
@deferred_load_size = args[:deferred_load_size] || (@hdr && @hdr.SizeOfRawData)
|
16
|
+
@image_base = args[:image_base] || 0
|
17
|
+
end
|
18
|
+
|
19
|
+
def name; @hdr.Name; end
|
20
|
+
def va ; @hdr.VirtualAddress + @image_base; end
|
21
|
+
def rva ; @hdr.VirtualAddress; end
|
22
|
+
def vsize; @hdr.VirtualSize; end
|
23
|
+
def flags; @hdr.Characteristics; end
|
24
|
+
def flags= f; @hdr.Characteristics= f; end
|
25
|
+
|
26
|
+
def data
|
27
|
+
if @data.empty? && @deferred_load_io && @deferred_load_pos && @deferred_load_size.to_i > 0
|
28
|
+
begin
|
29
|
+
old_pos = @deferred_load_io.tell
|
30
|
+
@deferred_load_io.seek @deferred_load_pos
|
31
|
+
@data = @deferred_load_io.binmode.read(@deferred_load_size) || EMPTY_DATA.dup
|
32
|
+
ensure
|
33
|
+
if @deferred_load_io && old_pos
|
34
|
+
@deferred_load_io.seek old_pos
|
35
|
+
@deferred_load_io = nil # prevent read only on 1st access to data
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
@data
|
40
|
+
end
|
41
|
+
|
42
|
+
def range
|
43
|
+
va...(va+vsize)
|
44
|
+
end
|
45
|
+
|
46
|
+
def inspect
|
47
|
+
r = "#<Section"
|
48
|
+
r << (" name=%-10s" % name.inspect) if name
|
49
|
+
r << " va=%8x vsize=%8x rawsize=%8s" % [
|
50
|
+
va, vsize,
|
51
|
+
@data.size > 0 ? @data.size.to_s(16) : (@deferred_load_io ? "<defer>" : 0)
|
52
|
+
]
|
53
|
+
r << (" dlpos=%8x" % @deferred_load_pos) if @deferred_load_pos
|
54
|
+
r << ">"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'awesome_print' # for colored tty logging
|
2
|
+
|
3
|
+
class PEdump
|
4
|
+
class Logger < ::Logger
|
5
|
+
def initialize *args
|
6
|
+
super
|
7
|
+
@formatter = proc do |severity,_,_,msg|
|
8
|
+
# quick and dirty way to remove duplicate messages
|
9
|
+
if @prevmsg == msg && severity != 'DEBUG' && severity != 'INFO'
|
10
|
+
''
|
11
|
+
else
|
12
|
+
@prevmsg = msg
|
13
|
+
"#{msg}\n"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
@level = WARN
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def Logger.create params
|
21
|
+
logger =
|
22
|
+
if params[:logger]
|
23
|
+
params[:logger]
|
24
|
+
else
|
25
|
+
logdev = params[:logdev] || STDERR
|
26
|
+
logger_class =
|
27
|
+
if params.key?(:color)
|
28
|
+
# forced color or not
|
29
|
+
params[:color] ? ColoredLogger : Logger
|
30
|
+
else
|
31
|
+
# set color if logdev is TTY
|
32
|
+
(logdev.respond_to?(:tty?) && logdev.tty?) ? ColoredLogger : Logger
|
33
|
+
end
|
34
|
+
logger_class.new(logdev)
|
35
|
+
end
|
36
|
+
|
37
|
+
logger.level = params[:log_level] if params[:log_level]
|
38
|
+
logger
|
39
|
+
end
|
40
|
+
|
41
|
+
class ColoredLogger < ::Logger
|
42
|
+
def initialize *args
|
43
|
+
super
|
44
|
+
@formatter = proc do |severity,_,_,msg|
|
45
|
+
# quick and dirty way to remove duplicate messages
|
46
|
+
if @prevmsg == msg && severity != 'DEBUG' && severity != 'INFO'
|
47
|
+
''
|
48
|
+
else
|
49
|
+
@prevmsg = msg
|
50
|
+
color =
|
51
|
+
case severity
|
52
|
+
when 'FATAL'
|
53
|
+
:redish
|
54
|
+
when 'ERROR'
|
55
|
+
:red
|
56
|
+
when 'WARN'
|
57
|
+
:yellowish
|
58
|
+
when 'DEBUG'
|
59
|
+
:gray
|
60
|
+
end
|
61
|
+
"#{color ? msg.send(color) : msg}\n"
|
62
|
+
end
|
63
|
+
end
|
64
|
+
@level = WARN
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|