jsobfu 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 +15 -0
- data/lib/jsobfu.rb +73 -0
- data/lib/jsobfu/ecma_tight.rb +316 -0
- data/lib/jsobfu/hoister.rb +84 -0
- data/lib/jsobfu/obfuscator.rb +144 -0
- data/lib/jsobfu/scope.rb +148 -0
- data/lib/jsobfu/utils.rb +366 -0
- data/samples/basic.rb +26 -0
- data/spec/integration_spec.rb +35 -0
- data/spec/jsobfu/hoister_spec.rb +68 -0
- data/spec/jsobfu/scope_spec.rb +201 -0
- data/spec/jsobfu/utils_spec.rb +156 -0
- data/spec/jsobfu_spec.rb +27 -0
- data/spec/spec_helper.rb +68 -0
- data/spec/support/matchers/be_in_charset.rb +5 -0
- data/spec/support/matchers/evaluate_to.rb +41 -0
- metadata +130 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
ZjQ5NmUzNTU1Mzk3ODdiZDNlMTkyNTQ5YWNmY2Y2MjJlNDVjNGZkMg==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
MGJhOTA3NDJiMDg4ODY4MDE3NDBkYWRiMGFjNjhhY2FiZjdiYzczZA==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NDA3NmNiYTI1MjM0ZjFhNWMyYTJjMWFkNTU2YTdhZjg4ODYxNWJlMmQwNjZh
|
10
|
+
MWRjMTUzZmUyNDcwMmJmNjYxZDliN2ViZTYyYjA2MWVmZmFkNzIzZGUwZDhj
|
11
|
+
ZDFmMDUwZmY2Y2JiMmNjNjU0MzA2ZjRiZTAyOGIwMzYxZjM5YWE=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ODI4ZTgyOTFkZDBlZTdhZDFkN2M0ODU2NWJkYmMwYjYwZTFiNTc2Yjg1OGEx
|
14
|
+
YzdhZTRmNjcwYTNhNzlkNTgxYzU0YjJjODVkNmIwYWE4ZWYwZTk3MTY4NzQ4
|
15
|
+
NDM4ODEzODQ4NTI0ODA3NTJhMWQwZjY1NDJlNzA3ZGFlOGQzN2I=
|
data/lib/jsobfu.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'rkelly'
|
2
|
+
|
3
|
+
# The primary class, used to parse and obfuscate Javascript code.
|
4
|
+
class JSObfu
|
5
|
+
|
6
|
+
require_relative 'jsobfu/scope'
|
7
|
+
require_relative 'jsobfu/utils'
|
8
|
+
require_relative 'jsobfu/ecma_tight'
|
9
|
+
require_relative 'jsobfu/hoister'
|
10
|
+
require_relative 'jsobfu/obfuscator'
|
11
|
+
|
12
|
+
# @return [JSObfu::Scope] the global scope
|
13
|
+
attr_reader :scope
|
14
|
+
|
15
|
+
# Saves +code+ for later obfuscation with #obfuscate
|
16
|
+
def initialize(code)
|
17
|
+
@code = code
|
18
|
+
@scope = Scope.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# Add +str+ to the un-obfuscated code.
|
22
|
+
# Calling this method after #obfuscate is undefined
|
23
|
+
def <<(str)
|
24
|
+
@code << str
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [String] the (possibly obfuscated) code
|
28
|
+
def to_s
|
29
|
+
@code
|
30
|
+
end
|
31
|
+
|
32
|
+
# @return [RKelly::Nodes::SourceElementsNode] the abstract syntax tree
|
33
|
+
def ast
|
34
|
+
@ast || parse
|
35
|
+
end
|
36
|
+
|
37
|
+
# Parse and obfuscate
|
38
|
+
#
|
39
|
+
# @param opts [Hash] the options hash
|
40
|
+
# @option opts [Boolean] :strip_whitespace allow whitespace in the output code
|
41
|
+
#
|
42
|
+
# @return [String] if successful
|
43
|
+
def obfuscate(opts={})
|
44
|
+
@obfuscator = JSObfu::Obfuscator.new(scope: @scope)
|
45
|
+
@code = @obfuscator.accept(ast).to_s
|
46
|
+
if opts.fetch(:strip_whitespace, true)
|
47
|
+
@code.gsub!(/(^\s+|\s+$)/, '')
|
48
|
+
@code.delete!("\n")
|
49
|
+
@code.delete!("\r")
|
50
|
+
end
|
51
|
+
self
|
52
|
+
end
|
53
|
+
|
54
|
+
# Returns the obfuscated name for the variable or function +sym+
|
55
|
+
#
|
56
|
+
# @param [String] sym the name of the variable or function
|
57
|
+
# @return [String] the obfuscated name
|
58
|
+
def sym(sym)
|
59
|
+
raise RuntimeError, "Must obfuscate before calling #sym" if @obfuscator.nil?
|
60
|
+
@obfuscator.renames[sym.to_s]
|
61
|
+
end
|
62
|
+
|
63
|
+
protected
|
64
|
+
|
65
|
+
#
|
66
|
+
# Generate an Abstract Syntax Tree (#ast) for later obfuscation
|
67
|
+
#
|
68
|
+
def parse
|
69
|
+
parser = RKelly::Parser.new
|
70
|
+
@ast = parser.parse(@code)
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,316 @@
|
|
1
|
+
#
|
2
|
+
# Implements an ECMAVisitor with minimal whitespace and indentation.
|
3
|
+
#
|
4
|
+
# Mostly a line-by-line copy of RKelly::Visitors::ECMAVisitor with the
|
5
|
+
# whitespace trimmed out.
|
6
|
+
#
|
7
|
+
class JSObfu::ECMANoWhitespaceVisitor < RKelly::Visitors::ECMAVisitor
|
8
|
+
|
9
|
+
def visit_ParentheticalNode(o)
|
10
|
+
"(#{o.value.accept(self)})"
|
11
|
+
end
|
12
|
+
|
13
|
+
def visit_SourceElementsNode(o)
|
14
|
+
o.value.map { |x| "#{x.accept(self)}" }.join
|
15
|
+
end
|
16
|
+
|
17
|
+
def visit_VarStatementNode(o)
|
18
|
+
"var #{o.value.map { |x| x.accept(self) }.join(',')};"
|
19
|
+
end
|
20
|
+
|
21
|
+
def visit_ConstStatementNode(o)
|
22
|
+
"const #{o.value.map { |x| x.accept(self) }.join(',')};"
|
23
|
+
end
|
24
|
+
|
25
|
+
def visit_VarDeclNode(o)
|
26
|
+
"#{o.name}#{o.value ? o.value.accept(self) : nil}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def visit_AssignExprNode(o)
|
30
|
+
"=#{o.value.accept(self)}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def visit_NumberNode(o)
|
34
|
+
o.value.to_s
|
35
|
+
end
|
36
|
+
|
37
|
+
def visit_ForNode(o)
|
38
|
+
init = o.init ? o.init.accept(self) : ';'
|
39
|
+
init << ';' unless init.end_with? ';' # make sure it has a ;
|
40
|
+
test = o.test ? o.test.accept(self) : ''
|
41
|
+
counter = o.counter ? o.counter.accept(self) : ''
|
42
|
+
"for(#{init}#{test};#{counter})#{o.value.accept(self)}"
|
43
|
+
end
|
44
|
+
|
45
|
+
def visit_LessNode(o)
|
46
|
+
"#{o.left.accept(self)}<#{o.value.accept(self)}"
|
47
|
+
end
|
48
|
+
|
49
|
+
def visit_ResolveNode(o)
|
50
|
+
o.value
|
51
|
+
end
|
52
|
+
|
53
|
+
def visit_PostfixNode(o)
|
54
|
+
"#{o.operand.accept(self)}#{o.value}"
|
55
|
+
end
|
56
|
+
|
57
|
+
def visit_PrefixNode(o)
|
58
|
+
"#{o.value}#{o.operand.accept(self)}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def visit_BlockNode(o)
|
62
|
+
"{#{o.value.accept(self)}}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def visit_ExpressionStatementNode(o)
|
66
|
+
"#{o.value.accept(self)};"
|
67
|
+
end
|
68
|
+
|
69
|
+
def visit_OpEqualNode(o)
|
70
|
+
"#{o.left.accept(self)}=#{o.value.accept(self)}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def visit_FunctionCallNode(o)
|
74
|
+
"#{o.value.accept(self)}(#{o.arguments.accept(self)})"
|
75
|
+
end
|
76
|
+
|
77
|
+
def visit_ArgumentsNode(o)
|
78
|
+
o.value.map { |x| x.accept(self) }.join(',')
|
79
|
+
end
|
80
|
+
|
81
|
+
def visit_StringNode(o)
|
82
|
+
o.value
|
83
|
+
end
|
84
|
+
|
85
|
+
def visit_NullNode(o)
|
86
|
+
"null"
|
87
|
+
end
|
88
|
+
|
89
|
+
def visit_FunctionDeclNode(o)
|
90
|
+
"function #{o.value}" + function_params_and_body(o)
|
91
|
+
end
|
92
|
+
|
93
|
+
def visit_ParameterNode(o)
|
94
|
+
o.value
|
95
|
+
end
|
96
|
+
|
97
|
+
def visit_FunctionBodyNode(o)
|
98
|
+
"{#{o.value.accept(self)}}"
|
99
|
+
end
|
100
|
+
|
101
|
+
def visit_BreakNode(o)
|
102
|
+
"break" + (o.value ? " #{o.value}" : '') + ';'
|
103
|
+
end
|
104
|
+
|
105
|
+
def visit_ContinueNode(o)
|
106
|
+
"continue" + (o.value ? " #{o.value}" : '') + ';'
|
107
|
+
end
|
108
|
+
|
109
|
+
def visit_TrueNode(o)
|
110
|
+
"true"
|
111
|
+
end
|
112
|
+
|
113
|
+
def visit_FalseNode(o)
|
114
|
+
"false"
|
115
|
+
end
|
116
|
+
|
117
|
+
def visit_EmptyStatementNode(o)
|
118
|
+
';'
|
119
|
+
end
|
120
|
+
|
121
|
+
def visit_RegexpNode(o)
|
122
|
+
o.value
|
123
|
+
end
|
124
|
+
|
125
|
+
def visit_DotAccessorNode(o)
|
126
|
+
"#{o.value.accept(self)}.#{o.accessor}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def visit_ThisNode(o)
|
130
|
+
"this"
|
131
|
+
end
|
132
|
+
|
133
|
+
def visit_BitwiseNotNode(o)
|
134
|
+
"~#{o.value.accept(self)}"
|
135
|
+
end
|
136
|
+
|
137
|
+
def visit_DeleteNode(o)
|
138
|
+
"delete #{o.value.accept(self)}"
|
139
|
+
end
|
140
|
+
|
141
|
+
def visit_ArrayNode(o)
|
142
|
+
"[#{o.value.map { |x| x ? x.accept(self) : '' }.join(',')}]"
|
143
|
+
end
|
144
|
+
|
145
|
+
def visit_ElementNode(o)
|
146
|
+
o.value.accept(self)
|
147
|
+
end
|
148
|
+
|
149
|
+
def visit_LogicalNotNode(o)
|
150
|
+
"!#{o.value.accept(self)}"
|
151
|
+
end
|
152
|
+
|
153
|
+
def visit_UnaryMinusNode(o)
|
154
|
+
"-#{o.value.accept(self)}"
|
155
|
+
end
|
156
|
+
|
157
|
+
def visit_UnaryPlusNode(o)
|
158
|
+
"+#{o.value.accept(self)}"
|
159
|
+
end
|
160
|
+
|
161
|
+
def visit_ReturnNode(o)
|
162
|
+
"return" + (o.value ? " #{o.value.accept(self)}" : '') + ';'
|
163
|
+
end
|
164
|
+
|
165
|
+
def visit_ThrowNode(o)
|
166
|
+
"throw #{o.value.accept(self)};"
|
167
|
+
end
|
168
|
+
|
169
|
+
def visit_TypeOfNode(o)
|
170
|
+
"typeof #{o.value.accept(self)}"
|
171
|
+
end
|
172
|
+
|
173
|
+
def visit_VoidNode(o)
|
174
|
+
"void(#{o.value.accept(self)})"
|
175
|
+
end
|
176
|
+
|
177
|
+
[
|
178
|
+
[:Add, '+'],
|
179
|
+
[:BitAnd, '&'],
|
180
|
+
[:BitOr, '|'],
|
181
|
+
[:BitXOr, '^'],
|
182
|
+
[:Divide, '/'],
|
183
|
+
[:Equal, '=='],
|
184
|
+
[:Greater, '>'],
|
185
|
+
[:GreaterOrEqual, '>='],
|
186
|
+
[:In, 'in'],
|
187
|
+
[:InstanceOf, 'instanceof'],
|
188
|
+
[:LeftShift, '<<'],
|
189
|
+
[:LessOrEqual, '<='],
|
190
|
+
[:LogicalAnd, '&&'],
|
191
|
+
[:LogicalOr, '||'],
|
192
|
+
[:Modulus, '%'],
|
193
|
+
[:Multiply, '*'],
|
194
|
+
[:NotEqual, '!='],
|
195
|
+
[:NotStrictEqual, '!=='],
|
196
|
+
[:OpAndEqual, '&='],
|
197
|
+
[:OpDivideEqual, '/='],
|
198
|
+
[:OpLShiftEqual, '<<='],
|
199
|
+
[:OpMinusEqual, '-='],
|
200
|
+
[:OpModEqual, '%='],
|
201
|
+
[:OpMultiplyEqual, '*='],
|
202
|
+
[:OpOrEqual, '|='],
|
203
|
+
[:OpPlusEqual, '+='],
|
204
|
+
[:OpRShiftEqual, '>>='],
|
205
|
+
[:OpURShiftEqual, '>>>='],
|
206
|
+
[:OpXOrEqual, '^='],
|
207
|
+
[:RightShift, '>>'],
|
208
|
+
[:StrictEqual, '==='],
|
209
|
+
[:Subtract, '-'],
|
210
|
+
[:UnsignedRightShift, '>>>'],
|
211
|
+
].each do |name,op|
|
212
|
+
define_method(:"visit_#{name}Node") do |o|
|
213
|
+
if op =~ /^[a-z]+$/
|
214
|
+
"#{o.left.accept(self)} #{op} #{o.value.accept(self)}"
|
215
|
+
else
|
216
|
+
"#{o.left.accept(self)}#{op}#{o.value.accept(self)}"
|
217
|
+
end
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def visit_WhileNode(o)
|
222
|
+
"while(#{o.left.accept(self)})#{o.value.accept(self)}"
|
223
|
+
end
|
224
|
+
|
225
|
+
def visit_SwitchNode(o)
|
226
|
+
"switch(#{o.left.accept(self)})#{o.value.accept(self)}"
|
227
|
+
end
|
228
|
+
|
229
|
+
def visit_CaseBlockNode(o)
|
230
|
+
"{" + (o.value ? o.value.map { |x| x.accept(self) }.join('') : '') + "}"
|
231
|
+
end
|
232
|
+
|
233
|
+
def visit_CaseClauseNode(o)
|
234
|
+
if o.left
|
235
|
+
case_code = "case #{o.left.accept(self)}:"
|
236
|
+
else
|
237
|
+
case_code = "default:"
|
238
|
+
end
|
239
|
+
case_code += "#{o.value.accept(self)}"
|
240
|
+
case_code
|
241
|
+
end
|
242
|
+
|
243
|
+
def visit_DoWhileNode(o)
|
244
|
+
"do#{o.left.accept(self)}while(#{o.value.accept(self)});"
|
245
|
+
end
|
246
|
+
|
247
|
+
def visit_WithNode(o)
|
248
|
+
"with(#{o.left.accept(self)})#{o.value.accept(self)}"
|
249
|
+
end
|
250
|
+
|
251
|
+
def visit_LabelNode(o)
|
252
|
+
"#{o.name}:#{o.value.accept(self)}"
|
253
|
+
end
|
254
|
+
|
255
|
+
def visit_ObjectLiteralNode(o)
|
256
|
+
"{" + o.value.map { |x| x.accept(self) }.join(",\n") + '}'
|
257
|
+
end
|
258
|
+
|
259
|
+
def visit_PropertyNode(o)
|
260
|
+
"#{o.name}:#{o.value.accept(self)}"
|
261
|
+
end
|
262
|
+
|
263
|
+
def visit_GetterPropertyNode(o)
|
264
|
+
"get #{o.name}" + function_params_and_body(o.value)
|
265
|
+
end
|
266
|
+
|
267
|
+
def visit_SetterPropertyNode(o)
|
268
|
+
"set #{o.name}" + function_params_and_body(o.value)
|
269
|
+
end
|
270
|
+
|
271
|
+
def visit_FunctionExprNode(o)
|
272
|
+
name = (o.value == 'function') ? '' : ' '+o.value
|
273
|
+
"function" + name + function_params_and_body(o)
|
274
|
+
end
|
275
|
+
|
276
|
+
# Helper for all the various function nodes
|
277
|
+
def function_params_and_body(o)
|
278
|
+
"(#{o.arguments.map { |x| x.accept(self) }.join(',')})" +
|
279
|
+
"#{o.function_body.accept(self)}"
|
280
|
+
end
|
281
|
+
|
282
|
+
def visit_CommaNode(o)
|
283
|
+
"#{o.left.accept(self)},#{o.value.accept(self)}"
|
284
|
+
end
|
285
|
+
|
286
|
+
def visit_IfNode(o)
|
287
|
+
"if(#{o.conditions.accept(self)})#{o.value.accept(self)}" +
|
288
|
+
(o.else ? "else #{o.else.accept(self)}" : '')
|
289
|
+
end
|
290
|
+
|
291
|
+
def visit_ConditionalNode(o)
|
292
|
+
"#{o.conditions.accept(self)}?#{o.value.accept(self)}:" +
|
293
|
+
"#{o.else.accept(self)}"
|
294
|
+
end
|
295
|
+
|
296
|
+
def visit_ForInNode(o)
|
297
|
+
var = o.left.is_a?(RKelly::Nodes::VarDeclNode) ? 'var ' : ''
|
298
|
+
"for(#{var}#{o.left.accept(self)} in #{o.right.accept(self)})" +
|
299
|
+
"#{o.value.accept(self)}"
|
300
|
+
end
|
301
|
+
|
302
|
+
def visit_TryNode(o)
|
303
|
+
"try#{o.value.accept(self)}" +
|
304
|
+
(o.catch_block ? " catch(#{o.catch_var})#{o.catch_block.accept(self)}" : '') +
|
305
|
+
(o.finally_block ? " finally#{o.finally_block.accept(self)}" : '')
|
306
|
+
end
|
307
|
+
|
308
|
+
def visit_BracketAccessorNode(o)
|
309
|
+
"#{o.value.accept(self)}[#{o.accessor.accept(self)}]"
|
310
|
+
end
|
311
|
+
|
312
|
+
def visit_NewExprNode(o)
|
313
|
+
"new #{o.value.accept(self)}(#{o.arguments.accept(self)})"
|
314
|
+
end
|
315
|
+
|
316
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
#
|
2
|
+
# Walks a Javascript AST and finds the immediate members of the
|
3
|
+
# root scope, which is useful for "hoisting" var and function
|
4
|
+
# declaration to the top of the function.
|
5
|
+
#
|
6
|
+
class JSObfu::Hoister < RKelly::Visitors::Visitor
|
7
|
+
|
8
|
+
# @return [Hash] the scope maintained while walking the ast
|
9
|
+
attr_reader :scope
|
10
|
+
|
11
|
+
# @return [Array<String>] the function names in the first level of this closure
|
12
|
+
attr_reader :functions
|
13
|
+
|
14
|
+
def initialize(opts={})
|
15
|
+
@parent_scope = opts.fetch(:parent_scope, nil)
|
16
|
+
@max_depth = 1
|
17
|
+
@depth = 0
|
18
|
+
@scope = {}
|
19
|
+
@resolves = []
|
20
|
+
@functions = []
|
21
|
+
super()
|
22
|
+
end
|
23
|
+
|
24
|
+
def visit_SourceElementsNode(o)
|
25
|
+
return if @max_depth and @depth >= @max_depth
|
26
|
+
@depth += 1
|
27
|
+
o.value.each { |x| x.accept(self) }
|
28
|
+
@depth -= 1
|
29
|
+
end
|
30
|
+
|
31
|
+
def visit_VarDeclNode(o)
|
32
|
+
scope[o.name] = o
|
33
|
+
end
|
34
|
+
|
35
|
+
def visit_FunctionDeclNode(o)
|
36
|
+
@functions << o.value
|
37
|
+
scope[o.value] = o
|
38
|
+
end
|
39
|
+
|
40
|
+
%w{
|
41
|
+
AddNode ArgumentsNode ArrayNode AssignExprNode BitAndNode BitOrNode
|
42
|
+
BitXOrNode BitwiseNotNode BlockNode BracketAccessorNode BreakNode
|
43
|
+
CaseBlockNode CaseClauseNode CommaNode ConditionalNode
|
44
|
+
ConstStatementNode ContinueNode DeleteNode DivideNode
|
45
|
+
DoWhileNode DotAccessorNode ElementNode EmptyStatementNode EqualNode
|
46
|
+
ExpressionStatementNode FalseNode ForInNode ForNode FunctionBodyNode
|
47
|
+
FunctionExprNode GetterPropertyNode GreaterNode GreaterOrEqualNode
|
48
|
+
IfNode InNode InstanceOfNode LabelNode LeftShiftNode LessNode
|
49
|
+
LessOrEqualNode LogicalAndNode LogicalNotNode LogicalOrNode ModulusNode
|
50
|
+
MultiplyNode NewExprNode NotEqualNode NotStrictEqualNode NullNode
|
51
|
+
NumberNode ObjectLiteralNode OpAndEqualNode OpDivideEqualNode
|
52
|
+
OpEqualNode OpLShiftEqualNode OpMinusEqualNode OpModEqualNode
|
53
|
+
OpMultiplyEqualNode OpOrEqualNode OpPlusEqualNode OpRShiftEqualNode
|
54
|
+
OpURShiftEqualNode OpXOrEqualNode ParameterNode PostfixNode PrefixNode
|
55
|
+
PropertyNode RegexpNode ResolveNode ReturnNode RightShiftNode
|
56
|
+
SetterPropertyNode StrictEqualNode StringNode
|
57
|
+
SubtractNode SwitchNode ThisNode ThrowNode TrueNode TryNode TypeOfNode
|
58
|
+
UnaryMinusNode UnaryPlusNode UnsignedRightShiftNode
|
59
|
+
VoidNode WhileNode WithNode
|
60
|
+
}.each do |type|
|
61
|
+
define_method(:"visit_#{type}") do |o|
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [String] Javascript that declares the discovered variables
|
66
|
+
def scope_declaration(opts={})
|
67
|
+
keys = scope.keys.dup
|
68
|
+
if opts.fetch(:shuffle, true)
|
69
|
+
keys = keys.shuffle
|
70
|
+
end
|
71
|
+
|
72
|
+
keys.delete_if { |k| @functions.include? k }
|
73
|
+
|
74
|
+
if @parent_scope
|
75
|
+
keys.delete_if { |k| @parent_scope.has_key? k }
|
76
|
+
keys.map! { |k| @parent_scope.renames[k.to_s] || k }
|
77
|
+
end
|
78
|
+
|
79
|
+
str = if keys.empty? then '' else "var #{keys.join(",")};" end
|
80
|
+
# puts str if str.length>0
|
81
|
+
str
|
82
|
+
end
|
83
|
+
|
84
|
+
end
|