immunio 0.15.2

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.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +234 -0
  3. data/README.md +147 -0
  4. data/bin/immunio +5 -0
  5. data/lib/immunio.rb +29 -0
  6. data/lib/immunio/agent.rb +260 -0
  7. data/lib/immunio/authentication.rb +96 -0
  8. data/lib/immunio/blocked_app.rb +38 -0
  9. data/lib/immunio/channel.rb +432 -0
  10. data/lib/immunio/cli.rb +39 -0
  11. data/lib/immunio/context.rb +114 -0
  12. data/lib/immunio/errors.rb +43 -0
  13. data/lib/immunio/immunio_ca.crt +45 -0
  14. data/lib/immunio/logger.rb +87 -0
  15. data/lib/immunio/plugins/action_dispatch.rb +45 -0
  16. data/lib/immunio/plugins/action_view.rb +431 -0
  17. data/lib/immunio/plugins/active_record.rb +707 -0
  18. data/lib/immunio/plugins/active_record_relation.rb +370 -0
  19. data/lib/immunio/plugins/authlogic.rb +80 -0
  20. data/lib/immunio/plugins/csrf.rb +24 -0
  21. data/lib/immunio/plugins/devise.rb +40 -0
  22. data/lib/immunio/plugins/environment_reporter.rb +69 -0
  23. data/lib/immunio/plugins/eval.rb +51 -0
  24. data/lib/immunio/plugins/exception_handler.rb +55 -0
  25. data/lib/immunio/plugins/gems_tracker.rb +5 -0
  26. data/lib/immunio/plugins/haml.rb +36 -0
  27. data/lib/immunio/plugins/http_finisher.rb +50 -0
  28. data/lib/immunio/plugins/http_tracker.rb +203 -0
  29. data/lib/immunio/plugins/io.rb +96 -0
  30. data/lib/immunio/plugins/redirect.rb +42 -0
  31. data/lib/immunio/plugins/warden.rb +66 -0
  32. data/lib/immunio/processor.rb +234 -0
  33. data/lib/immunio/rails.rb +26 -0
  34. data/lib/immunio/request.rb +139 -0
  35. data/lib/immunio/rufus_lua_ext/ref.rb +27 -0
  36. data/lib/immunio/rufus_lua_ext/state.rb +157 -0
  37. data/lib/immunio/rufus_lua_ext/table.rb +137 -0
  38. data/lib/immunio/rufus_lua_ext/utils.rb +13 -0
  39. data/lib/immunio/version.rb +5 -0
  40. data/lib/immunio/vm.rb +291 -0
  41. data/lua-hooks/ext/all.c +78 -0
  42. data/lua-hooks/ext/bitop/README +22 -0
  43. data/lua-hooks/ext/bitop/bit.c +189 -0
  44. data/lua-hooks/ext/extconf.rb +38 -0
  45. data/lua-hooks/ext/libinjection/COPYING +37 -0
  46. data/lua-hooks/ext/libinjection/libinjection.h +65 -0
  47. data/lua-hooks/ext/libinjection/libinjection_html5.c +847 -0
  48. data/lua-hooks/ext/libinjection/libinjection_html5.h +54 -0
  49. data/lua-hooks/ext/libinjection/libinjection_sqli.c +2301 -0
  50. data/lua-hooks/ext/libinjection/libinjection_sqli.h +295 -0
  51. data/lua-hooks/ext/libinjection/libinjection_sqli_data.h +9349 -0
  52. data/lua-hooks/ext/libinjection/libinjection_xss.c +531 -0
  53. data/lua-hooks/ext/libinjection/libinjection_xss.h +21 -0
  54. data/lua-hooks/ext/libinjection/lualib.c +109 -0
  55. data/lua-hooks/ext/lpeg/HISTORY +90 -0
  56. data/lua-hooks/ext/lpeg/lpcap.c +537 -0
  57. data/lua-hooks/ext/lpeg/lpcap.h +43 -0
  58. data/lua-hooks/ext/lpeg/lpcode.c +986 -0
  59. data/lua-hooks/ext/lpeg/lpcode.h +34 -0
  60. data/lua-hooks/ext/lpeg/lpeg-128.gif +0 -0
  61. data/lua-hooks/ext/lpeg/lpeg.html +1429 -0
  62. data/lua-hooks/ext/lpeg/lpprint.c +244 -0
  63. data/lua-hooks/ext/lpeg/lpprint.h +35 -0
  64. data/lua-hooks/ext/lpeg/lptree.c +1238 -0
  65. data/lua-hooks/ext/lpeg/lptree.h +77 -0
  66. data/lua-hooks/ext/lpeg/lptypes.h +149 -0
  67. data/lua-hooks/ext/lpeg/lpvm.c +355 -0
  68. data/lua-hooks/ext/lpeg/lpvm.h +58 -0
  69. data/lua-hooks/ext/lpeg/makefile +55 -0
  70. data/lua-hooks/ext/lpeg/re.html +498 -0
  71. data/lua-hooks/ext/lpeg/test.lua +1409 -0
  72. data/lua-hooks/ext/lua-cmsgpack/CMakeLists.txt +45 -0
  73. data/lua-hooks/ext/lua-cmsgpack/README.md +115 -0
  74. data/lua-hooks/ext/lua-cmsgpack/lua_cmsgpack.c +957 -0
  75. data/lua-hooks/ext/lua-cmsgpack/test.lua +570 -0
  76. data/lua-hooks/ext/lua-snapshot/LICENSE +7 -0
  77. data/lua-hooks/ext/lua-snapshot/Makefile +12 -0
  78. data/lua-hooks/ext/lua-snapshot/README.md +18 -0
  79. data/lua-hooks/ext/lua-snapshot/dump.lua +15 -0
  80. data/lua-hooks/ext/lua-snapshot/snapshot.c +455 -0
  81. data/lua-hooks/ext/lua/COPYRIGHT +34 -0
  82. data/lua-hooks/ext/lua/lapi.c +1087 -0
  83. data/lua-hooks/ext/lua/lapi.h +16 -0
  84. data/lua-hooks/ext/lua/lauxlib.c +652 -0
  85. data/lua-hooks/ext/lua/lauxlib.h +174 -0
  86. data/lua-hooks/ext/lua/lbaselib.c +659 -0
  87. data/lua-hooks/ext/lua/lcode.c +831 -0
  88. data/lua-hooks/ext/lua/lcode.h +76 -0
  89. data/lua-hooks/ext/lua/ldblib.c +398 -0
  90. data/lua-hooks/ext/lua/ldebug.c +638 -0
  91. data/lua-hooks/ext/lua/ldebug.h +33 -0
  92. data/lua-hooks/ext/lua/ldo.c +519 -0
  93. data/lua-hooks/ext/lua/ldo.h +57 -0
  94. data/lua-hooks/ext/lua/ldump.c +164 -0
  95. data/lua-hooks/ext/lua/lfunc.c +174 -0
  96. data/lua-hooks/ext/lua/lfunc.h +34 -0
  97. data/lua-hooks/ext/lua/lgc.c +710 -0
  98. data/lua-hooks/ext/lua/lgc.h +110 -0
  99. data/lua-hooks/ext/lua/linit.c +38 -0
  100. data/lua-hooks/ext/lua/liolib.c +556 -0
  101. data/lua-hooks/ext/lua/llex.c +463 -0
  102. data/lua-hooks/ext/lua/llex.h +81 -0
  103. data/lua-hooks/ext/lua/llimits.h +128 -0
  104. data/lua-hooks/ext/lua/lmathlib.c +263 -0
  105. data/lua-hooks/ext/lua/lmem.c +86 -0
  106. data/lua-hooks/ext/lua/lmem.h +49 -0
  107. data/lua-hooks/ext/lua/loadlib.c +705 -0
  108. data/lua-hooks/ext/lua/loadlib_rel.c +760 -0
  109. data/lua-hooks/ext/lua/lobject.c +214 -0
  110. data/lua-hooks/ext/lua/lobject.h +381 -0
  111. data/lua-hooks/ext/lua/lopcodes.c +102 -0
  112. data/lua-hooks/ext/lua/lopcodes.h +268 -0
  113. data/lua-hooks/ext/lua/loslib.c +243 -0
  114. data/lua-hooks/ext/lua/lparser.c +1339 -0
  115. data/lua-hooks/ext/lua/lparser.h +82 -0
  116. data/lua-hooks/ext/lua/lstate.c +214 -0
  117. data/lua-hooks/ext/lua/lstate.h +169 -0
  118. data/lua-hooks/ext/lua/lstring.c +111 -0
  119. data/lua-hooks/ext/lua/lstring.h +31 -0
  120. data/lua-hooks/ext/lua/lstrlib.c +871 -0
  121. data/lua-hooks/ext/lua/ltable.c +588 -0
  122. data/lua-hooks/ext/lua/ltable.h +40 -0
  123. data/lua-hooks/ext/lua/ltablib.c +287 -0
  124. data/lua-hooks/ext/lua/ltm.c +75 -0
  125. data/lua-hooks/ext/lua/ltm.h +54 -0
  126. data/lua-hooks/ext/lua/lua.c +392 -0
  127. data/lua-hooks/ext/lua/lua.def +131 -0
  128. data/lua-hooks/ext/lua/lua.h +388 -0
  129. data/lua-hooks/ext/lua/lua.rc +28 -0
  130. data/lua-hooks/ext/lua/lua_dll.rc +26 -0
  131. data/lua-hooks/ext/lua/luac.c +200 -0
  132. data/lua-hooks/ext/lua/luac.rc +1 -0
  133. data/lua-hooks/ext/lua/luaconf.h +763 -0
  134. data/lua-hooks/ext/lua/luaconf.h.in +724 -0
  135. data/lua-hooks/ext/lua/luaconf.h.orig +763 -0
  136. data/lua-hooks/ext/lua/lualib.h +53 -0
  137. data/lua-hooks/ext/lua/lundump.c +227 -0
  138. data/lua-hooks/ext/lua/lundump.h +36 -0
  139. data/lua-hooks/ext/lua/lvm.c +767 -0
  140. data/lua-hooks/ext/lua/lvm.h +36 -0
  141. data/lua-hooks/ext/lua/lzio.c +82 -0
  142. data/lua-hooks/ext/lua/lzio.h +67 -0
  143. data/lua-hooks/ext/lua/print.c +227 -0
  144. data/lua-hooks/ext/luautf8/README.md +152 -0
  145. data/lua-hooks/ext/luautf8/lutf8lib.c +1274 -0
  146. data/lua-hooks/ext/luautf8/unidata.h +3064 -0
  147. data/lua-hooks/lib/boot.lua +254 -0
  148. data/lua-hooks/lib/encode.lua +4 -0
  149. data/lua-hooks/lib/lexers/LICENSE +21 -0
  150. data/lua-hooks/lib/lexers/bash.lua +134 -0
  151. data/lua-hooks/lib/lexers/bash_dqstr.lua +62 -0
  152. data/lua-hooks/lib/lexers/css.lua +216 -0
  153. data/lua-hooks/lib/lexers/html.lua +106 -0
  154. data/lua-hooks/lib/lexers/javascript.lua +68 -0
  155. data/lua-hooks/lib/lexers/lexer.lua +1575 -0
  156. data/lua-hooks/lib/lexers/markers.lua +33 -0
  157. metadata +308 -0
@@ -0,0 +1,139 @@
1
+ require_relative "vm"
2
+
3
+ module Immunio
4
+ class Request
5
+ # Data sent to the server
6
+ attr_accessor :data, :vm
7
+
8
+ # We keep the VM and hook handlers in the request to ensure all hooks for this request
9
+ # use the same handlers.
10
+ attr_accessor :vm
11
+
12
+ def initialize(env)
13
+ @env = env
14
+ @data = {
15
+ "type" => "engine.http_request",
16
+ "request_id" => id,
17
+ "start_time" => Time.now.utc.iso8601(6),
18
+ }
19
+ @timings = Hash.new { |h, name| h[name] = { count: 0, total_duration: 0 } }
20
+ @paused_times = {}
21
+ @start_time = Time.now
22
+
23
+ Immunio.logger.debug "Created new Request #{id} with data #{@data}"
24
+ end
25
+
26
+ def id
27
+ @env["action_dispatch.request_id"] || internal_id
28
+ end
29
+
30
+ # Time the execution of a block and stores it in the request's `timings`.
31
+ def time(type, name)
32
+ Immunio.logger.debug { "Starting request time for #{type} #{name}" }
33
+ start = Time.now
34
+ key = {type: type, name: name}
35
+ nested = false
36
+ if @paused_times[key]
37
+ nested = true
38
+ else
39
+ @paused_times[key] = 0
40
+ end
41
+ yield
42
+ ensure
43
+ timing = @timings[key]
44
+ timing[:count] += 1
45
+ timing[:total_duration] += (Time.now - start) * 1000
46
+ if !nested
47
+ timing[:total_duration] -= @paused_times.delete(key)
48
+ end
49
+ end
50
+
51
+ # Pause execution timing while executing block
52
+ def pause(type, name)
53
+ key = {type: type, name: name}
54
+ start = Time.now
55
+ yield
56
+ ensure
57
+ @paused_times[key]+= (Time.now - start) * 1000
58
+ end
59
+
60
+ def timings
61
+ total_key = {type: "request", name: "total"}
62
+ timing = @timings[total_key]
63
+ timing[:count] = 1
64
+ timing[:total_duration] = (Time.now - @start_time) * 1000
65
+
66
+ split_timings = {}
67
+ @timings.each do |key, value|
68
+ # Round to microsecond precision
69
+ value[:total_duration] = value[:total_duration].round(3)
70
+
71
+ split_timings[key[:type]] ||= {}
72
+ split_timings[key[:type]][key[:name]] = value
73
+ end
74
+ split_timings
75
+ end
76
+
77
+ def should_report?
78
+ result = Immunio.run_hook "request", "should_report"
79
+ # If the hook doesn't exist, turn a nil report value into a false value
80
+ report = !!result["report"]
81
+ Immunio.logger.debug "Report request: #{report}"
82
+ report
83
+ end
84
+
85
+ # Encode the request data to send it to the channel.
86
+ def encode
87
+ if @data.is_a? Hash
88
+ @data.to_msgpack
89
+ else # Lua table
90
+ # Encode the data in Lua to avoid pulling a huge Lua table out of the VM, which is slow.
91
+ @vm.unsafe_call_by_name 'encode', @data
92
+ end
93
+ end
94
+
95
+ def self.current
96
+ Thread.current["immunio.request"]
97
+ end
98
+
99
+ def self.current=(request)
100
+ Thread.current["immunio.request"] = request
101
+ Immunio.logger.debug "Setting current request for thread #{Thread.current.object_id} to #{request && request.id || nil}"
102
+ end
103
+
104
+ # This shim exists to preserve the stack depth into hooks when self.time and self.pause are
105
+ # called. Otherwise the number of immunio frames will vary based on the state of the request object
106
+ def self.stack_shim()
107
+ yield
108
+ end
109
+
110
+ def self.time(type, name, &block)
111
+ if Request.current
112
+ Request.current.time(type, name, &block)
113
+ else
114
+ Immunio.logger.debug { "Starting request time for #{type} #{name}" }
115
+ self.stack_shim(&block)
116
+ end
117
+ end
118
+
119
+ def self.pause(type, name, &block)
120
+ if Request.current
121
+ Request.current.pause(type, name, &block)
122
+ else
123
+ self.stack_shim(&block)
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def internal_id
130
+ return @internal_id if defined?(@internal_id)
131
+
132
+ if @env['HTTP_X_REQUEST_ID']
133
+ @internal_id = @env['HTTP_X_REQUEST_ID'].gsub(/[^\w\-]/, '').first(255)
134
+ else
135
+ @internal_id = SecureRandom.uuid
136
+ end
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,27 @@
1
+ module Rufus::Lua
2
+ # Auto-unref Lua ref when it goes out of scope in Ruby.
3
+ class Ref
4
+ def initialize_with_finalizer(*args)
5
+ initialize_without_finalizer(*args)
6
+
7
+ ObjectSpace.define_finalizer self, self.class.finalize(@pointer, @ref)
8
+ end
9
+ alias_method :initialize_without_finalizer, :initialize
10
+ alias_method :initialize, :initialize_with_finalizer
11
+
12
+ def self.finalize(pointer, ref)
13
+ # Hack to test if the Lua state was closed and de-refed.
14
+ original_pointer_address = pointer.read_pointer
15
+ proc do
16
+ Lib.luaL_unref(pointer, LUA_REGISTRYINDEX, ref) if original_pointer_address == pointer.read_pointer
17
+ end
18
+ end
19
+
20
+ undef :free
21
+ def free
22
+ # Noop. Taken care of by finalizer. Should not conflict.
23
+ end
24
+
25
+ public :load_onto_stack
26
+ end
27
+ end
@@ -0,0 +1,157 @@
1
+ module Rufus::Lua
2
+ class State
3
+ # Load the code into a function and return it.
4
+ # Safer than evaling some string.
5
+ def load(code, filename=nil)
6
+ err = Lib.luaL_loadbuffer(@pointer, code, code.size, filename)
7
+ fail_if_error('load:compile', err, nil, filename, nil)
8
+
9
+ function = stack_fetch
10
+ stack_pop
11
+
12
+ function
13
+ end
14
+ end
15
+
16
+ class HashWithNilKeyError < StandardError
17
+ end
18
+
19
+ module StateMixin
20
+ # Expose error_handler to set it manually on Functions since it is not inherited from State.
21
+ attr_accessor :error_handler
22
+
23
+ protected
24
+
25
+ # Fix stack_fetch so it doesn't pop a value off the top for tables,
26
+ # functions, and threads.
27
+ #
28
+ # FIXME: pos isn't honored for tables, functions, or threads.
29
+ undef :stack_fetch
30
+ def stack_fetch(pos=-1)
31
+ type, tname = stack_type_at(pos)
32
+
33
+ case type
34
+
35
+ when TNIL then nil
36
+
37
+ when TSTRING then
38
+ string = nil
39
+ FFI::MemoryPointer.new(:size_t) do |len|
40
+ ptr = Lib.lua_tolstring(@pointer, pos, len)
41
+ string = ptr.read_string(len.read_long)
42
+ end
43
+ string
44
+
45
+ when TBOOLEAN then (Lib.lua_toboolean(@pointer, pos) == 1)
46
+ when TNUMBER then Lib.lua_tonumber(@pointer, pos)
47
+
48
+ when TTABLE then
49
+ table = Table.new(@pointer)
50
+ table.load_onto_stack
51
+ table
52
+
53
+ when TFUNCTION then
54
+ func = Function.new(@pointer)
55
+ func.load_onto_stack
56
+ func
57
+
58
+ when TTHREAD then
59
+ coroutine = Coroutine.new(@pointer)
60
+ coroutine.load_onto_stack
61
+ coroutine
62
+
63
+ else tname
64
+ end
65
+ end
66
+
67
+ # Now that we've fixed popping in stack_fetch above, always remove the top
68
+ # item from the stack.
69
+ undef :stack_pop
70
+ def stack_pop
71
+ r = stack_fetch
72
+ stack_unstack
73
+ r
74
+ end
75
+
76
+ # This method is effectively a pop without retrieving the top value.
77
+ # Upstream papers over stack corruption by not letting the stack pointer
78
+ # go negative, but this makes our tests erroneously pass when they check
79
+ # for stack corruption. Instead, do the right thing, and catch problems in
80
+ # tests.
81
+ undef :stack_unstack
82
+ def stack_unstack
83
+ Lib.lua_settop(@pointer, -2)
84
+ end
85
+
86
+ # Patch `stack_push` to support passing Lua objects (functions, tables)
87
+ # back into the VM. Also, support conversion of Pathname objects.
88
+ def stack_push_with_ref(o)
89
+ case o
90
+ when Ref
91
+ o.load_onto_stack
92
+ when Pathname
93
+ stack_push_without_ref o.to_s
94
+ when File
95
+ stack_push_without_ref o.path
96
+ when ActiveSupport::Multibyte::Chars
97
+ stack_push_without_ref o.to_s
98
+ else
99
+ begin
100
+ stack_push_without_ref o
101
+ rescue ArgumentError
102
+ # Rufus-lua doesn't know how to handle it, so call inspect which
103
+ # should always return a string describing the object. Usually we
104
+ # just need a placeholder for such objects, so this will do.
105
+ stack_push_without_ref o.inspect
106
+ end
107
+ end
108
+ end
109
+ alias_method :stack_push_without_ref, :stack_push
110
+ alias_method :stack_push, :stack_push_with_ref
111
+
112
+ # Patch `stack_fetch` to set encoding to strings.
113
+ def stack_fetch_with_unicode(pos=-1)
114
+ type = stack_type_at(pos)
115
+
116
+ case type[0]
117
+ when TSTRING
118
+ string = nil
119
+ FFI::MemoryPointer.new(:size_t) do |len|
120
+ ptr = Lib.lua_tolstring(@pointer, pos, len)
121
+ string = ptr.read_string(len.read_long)
122
+ end
123
+ string.force_encoding(Encoding.default_internal)
124
+ else
125
+ stack_fetch_without_unicode pos
126
+ end
127
+ end
128
+ alias_method :stack_fetch_without_unicode, :stack_fetch
129
+ alias_method :stack_fetch, :stack_fetch_with_unicode
130
+
131
+ # Trying to push a hash with a nil key causes a Lua panic. Instead, raise
132
+ # an exception in Ruby which we can deal with.
133
+ def stack_push_hash_with_immunio(h)
134
+ if h.has_key? nil
135
+ raise HashWithNilKeyError.new "can't pass hash with nil key into Lua"
136
+ end
137
+ stack_push_hash_without_immunio h
138
+ end
139
+ alias_method :stack_push_hash_without_immunio, :stack_push_hash
140
+ alias_method :stack_push_hash, :stack_push_hash_with_immunio
141
+ end
142
+
143
+ class Function
144
+ # If an argument fails to be converted for Lua, we end up leaking everything
145
+ # that was already pushed onto the stack for the call. Make sure we reset
146
+ # our stack.
147
+ def call_with_stack_reset(*args)
148
+ begin
149
+ call_without_stack_reset(*args)
150
+ rescue
151
+ Lib.lua_settop(@pointer, 0)
152
+ raise
153
+ end
154
+ end
155
+ alias_method_chain :call, :stack_reset
156
+ end
157
+ end
@@ -0,0 +1,137 @@
1
+ module Rufus::Lua
2
+ class Table
3
+ # Method to behave like Hash#fetch.
4
+ def fetch(key, default=nil)
5
+ value = self[key]
6
+ if value.nil?
7
+ default
8
+ else
9
+ value
10
+ end
11
+ end
12
+
13
+ def has_key?(key)
14
+ self.fetch(key) != nil
15
+ end
16
+
17
+ # Fix memory leak
18
+ undef :"[]"
19
+ def [](k)
20
+ # Push table onto stack
21
+ load_onto_stack
22
+
23
+ # Push key onto stack
24
+ stack_push(k)
25
+
26
+ # Fetches the value from the table, replaces the key at the top of the
27
+ # stack with the value. At the end, the stack still has the table at -2.
28
+ Lib.lua_gettable(@pointer, -2)
29
+
30
+ # Pop value off stack
31
+ v = stack_pop
32
+
33
+ # Pop table off stack
34
+ stack_pop # <= Memory leak fixed here
35
+
36
+ v
37
+ end
38
+
39
+ # Fix memory leak
40
+ undef :"[]="
41
+ def []=(k, v)
42
+ # Push table onto stack
43
+ load_onto_stack
44
+
45
+ stack_push(k)
46
+ stack_push(v)
47
+
48
+ # For the VM heavy thread test, we need to cause out-of-order execution in
49
+ # methods modifying the Lua stack. Here we tell Ruby to swap out threads
50
+ # if we are in the thread test.
51
+ Thread.pass if $IMMUNIO_IN_THREAD_TEST
52
+
53
+ # Sets the key-value pair onto the table, then pops the key and value but
54
+ # leaves the table on the stack
55
+ Lib.lua_settable(@pointer, -3)
56
+
57
+ # Pop table off stack
58
+ stack_pop # <= Memory leak fixed here
59
+
60
+ v
61
+ end
62
+
63
+ # Fix memory leak
64
+ undef :objlen
65
+ def objlen
66
+ # Push table onto stack
67
+ load_onto_stack
68
+
69
+ # Gets the length of the table, but leaves the table on the stack
70
+ len = Lib.lua_objlen(@pointer, -1)
71
+
72
+ # Pop table off stack
73
+ stack_pop # <= Memory leak fixed here
74
+
75
+ len
76
+ end
77
+
78
+ # Override original to_h to fix bad implementation and convert nested
79
+ # collections.
80
+ # See http://www.lua.org/manual/5.1/manual.html#lua%5Fnext for details on
81
+ # this crazy complex iteration system in lua...
82
+ undef :to_h
83
+ def to_h
84
+ # Load table onto stack
85
+ load_onto_stack
86
+
87
+ # Get position of table on stack
88
+ table_pos = stack_top
89
+
90
+ # This is the starter for the lua_next iterator. Push a nil value as a
91
+ # sentinel onto the stack. This tells lua_next to fetch the first key-
92
+ # value pair from the table.
93
+ Lib.lua_pushnil(@pointer)
94
+
95
+ h = {}
96
+
97
+ # Fetch the next key-value pair. The last key must be on the top of the
98
+ # stack. The key on the stack will be overwritten with the next key
99
+ # fetched from the table. The value will be pushed onto the stack after
100
+ # the key.
101
+ #
102
+ # In order to iterate, the caller must pop the value off the stack.
103
+ # lua_next then uses the current key, which is once again at the top of
104
+ # the stack, to locate the next key.
105
+ #
106
+ # But! When there are no more key-value pairs, this call will remove the
107
+ # key from the stack, leaving the table at the top.
108
+ #
109
+ # WARNING: Lua keys can be anything (yes, that includes a table). We do
110
+ # not support tables as keys. Keys must be nice numbers or strings.
111
+ while Lib.lua_next(@pointer, table_pos) != 0 do
112
+ # Grab key and value from top of stack
113
+ value = stack_fetch(-1)
114
+ key = stack_fetch(-2)
115
+
116
+ # Pop the value off the stack leaving the key on top
117
+ stack_unstack
118
+
119
+ # Recurse into table if needed
120
+ if Table === value
121
+ if value[1]
122
+ h[key] = value.to_a
123
+ else
124
+ h[key] = value.to_h
125
+ end
126
+ else
127
+ h[key] = value
128
+ end
129
+ end
130
+
131
+ # Remove the table pushed onto the stack at the very beginning
132
+ Lib.lua_remove(@pointer, table_pos)
133
+
134
+ h
135
+ end
136
+ end
137
+ end