bijou 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/ChangeLog.txt +4 -0
  2. data/LICENSE.txt +58 -0
  3. data/README.txt +48 -0
  4. data/Rakefile +105 -0
  5. data/doc/INSTALL.rdoc +260 -0
  6. data/doc/README.rdoc +314 -0
  7. data/doc/releases/bijou-0.1.0.rdoc +60 -0
  8. data/examples/birthday/birthday.rb +34 -0
  9. data/examples/holiday/holiday.rb +61 -0
  10. data/examples/holiday/letterhead.txt +4 -0
  11. data/examples/holiday/signature.txt +9 -0
  12. data/examples/phishing/letter.txt +29 -0
  13. data/examples/phishing/letterhead.txt +4 -0
  14. data/examples/phishing/phishing.rb +21 -0
  15. data/examples/phishing/signature.txt +9 -0
  16. data/examples/profile/profile.rb +46 -0
  17. data/lib/bijou.rb +15 -0
  18. data/lib/bijou/backend.rb +542 -0
  19. data/lib/bijou/cgi/adapter.rb +201 -0
  20. data/lib/bijou/cgi/handler.rb +5 -0
  21. data/lib/bijou/cgi/request.rb +37 -0
  22. data/lib/bijou/common.rb +12 -0
  23. data/lib/bijou/component.rb +108 -0
  24. data/lib/bijou/config.rb +60 -0
  25. data/lib/bijou/console/adapter.rb +167 -0
  26. data/lib/bijou/console/handler.rb +4 -0
  27. data/lib/bijou/console/request.rb +26 -0
  28. data/lib/bijou/context.rb +431 -0
  29. data/lib/bijou/diagnostics.rb +87 -0
  30. data/lib/bijou/errorformatter.rb +322 -0
  31. data/lib/bijou/exception.rb +39 -0
  32. data/lib/bijou/filters.rb +107 -0
  33. data/lib/bijou/httprequest.rb +108 -0
  34. data/lib/bijou/httpresponse.rb +268 -0
  35. data/lib/bijou/lexer.rb +513 -0
  36. data/lib/bijou/minicgi.rb +159 -0
  37. data/lib/bijou/parser.rb +1026 -0
  38. data/lib/bijou/processor.rb +404 -0
  39. data/lib/bijou/prstringio.rb +400 -0
  40. data/lib/bijou/webrick/adapter.rb +174 -0
  41. data/lib/bijou/webrick/handler.rb +32 -0
  42. data/lib/bijou/webrick/request.rb +45 -0
  43. data/script/cgi.rb +25 -0
  44. data/script/console.rb +7 -0
  45. data/script/server.rb +7 -0
  46. data/test/t1.cfg +5 -0
  47. data/test/tc_config.rb +26 -0
  48. data/test/tc_filter.rb +25 -0
  49. data/test/tc_lexer.rb +120 -0
  50. data/test/tc_response.rb +103 -0
  51. data/test/tc_ruby.rb +62 -0
  52. data/test/tc_stack.rb +50 -0
  53. metadata +121 -0
@@ -0,0 +1,404 @@
1
+ #
2
+ # Copyright (c) 2007-2008 Todd Lucas. All rights reserved.
3
+ #
4
+ # processor.rb - The Bijou component loader and runtime dispatcher
5
+ #
6
+ require 'fileutils'
7
+ require 'digest/sha1'
8
+ require 'bijou/common'
9
+ require 'bijou/exception'
10
+ require 'bijou/config'
11
+ require 'bijou/parser'
12
+ require 'bijou/context'
13
+ require 'bijou/exception'
14
+ require 'bijou/errorformatter'
15
+
16
+ module Bijou
17
+ #
18
+ # The processor encapsulates the loading and parsing of a component class and
19
+ # any referenced containers or components.
20
+ #
21
+ class Processor
22
+ ComponentPrefix = 'Component_'
23
+ CacheSuffix = '_cache'
24
+ CacheSubdir = 'cache'
25
+ PathSepRE = /\/|\\/
26
+
27
+ def initialize()
28
+ @trace = false
29
+ end
30
+
31
+ # Returns true if the file is cached and the cache is fresh.
32
+ def cache_check(config, filename, cachename)
33
+ if !config.cache
34
+ # Don't cache anything
35
+ trace "don't cache"
36
+ return false
37
+ end
38
+
39
+ return !Processor.is_cache_stale(filename, cachename)
40
+ end
41
+
42
+ def trace(msg)
43
+ puts msg if @trace
44
+ end
45
+
46
+ # Returns a cache directory based on the configuration.
47
+ def get_cache_root(config)
48
+ if config.cache_root
49
+ # Store in the cache subdirectory of the cache root. We do this is
50
+ # in case we need to store additional files.
51
+ cache_root = File.expand_path('cache', config.cache_root)
52
+ elsif config.cache_ext
53
+ # Use the specified extension and store in the document root.
54
+ cache_root = config.document_root
55
+ else
56
+ # Neither specified; create a cache root parallel to the doc root.
57
+
58
+ # Strip the trailing slash.
59
+ cache_root = File.expand_path('', config.document_root)
60
+
61
+ # Give it a different path name, parallel to the document root.
62
+ cache_root << CacheSuffix
63
+
64
+ # Store in the cache subdirectory of the cache root. We do this is
65
+ # in case we need to store additional files.
66
+ cache_root = File.expand_path('cache', cache_root)
67
+ end
68
+
69
+ return cache_root
70
+ end
71
+
72
+ # Returns true if the file is cached and the cache is fresh.
73
+ def get_cache_path(config, path)
74
+ cache_root = get_cache_root(config)
75
+
76
+ cache_path = File.expand_path(path, cache_root)
77
+ if config.cache_ext
78
+ cache_path << config.cache_ext
79
+ end
80
+
81
+ return cache_path
82
+ end
83
+
84
+ def create_class_object(class_name, context)
85
+ component_class = eval(class_name)
86
+ return component_class.new(context)
87
+ end
88
+
89
+ # Handles loading of containers. The context invokes this callback when
90
+ # a component is loaded and the component has an associated container. The
91
+ # context class is environment agnostic and thus doesn't know how to load
92
+ # a container. Instead, it delegates the mechanics to the context owner.
93
+ def load_container_callback(context, path)
94
+ trace "container_callback #{path}"
95
+
96
+ load_component(context, path)
97
+ end
98
+
99
+ # When a component is requested, either directly, by context.invoke, or
100
+ # indirectly, using the <& ... &> syntax, the context delegates the loading
101
+ # of the component to the owner of the context (if the component_callback
102
+ # was registered).
103
+ def load_component_callback(context, path, args)
104
+ trace "component_callback #{path}"
105
+
106
+ subcontext = context.clone
107
+
108
+ load_component(subcontext, path)
109
+
110
+ subcontext.render(args)
111
+ return subcontext.output
112
+ end
113
+
114
+ # This is the workhorse of the processor. It utilizes several classes,
115
+ # including the parser, to load and parse a component in preparation for
116
+ # rendering. The return value is a Context object that may be used to
117
+ # render the component one or more times, each with a different set of
118
+ # arguments.
119
+ #
120
+ # The cfg argument may be either a Bijou::Config or a Bijou::Context.
121
+ #
122
+ def load(path, cfg=nil)
123
+ context = nil
124
+ config = nil
125
+
126
+ # Initialize the context and config.
127
+ if cfg
128
+ if cfg.kind_of?(Bijou::Config)
129
+ config = cfg
130
+ elsif cfg.kind_of?(Bijou::Context)
131
+ context = cfg
132
+ config = context.config
133
+ else
134
+ raise "If specified, cfg must be a Context or Config object"
135
+ end
136
+ else
137
+ # Provide a default.
138
+ config = Bijou::Config.new
139
+ end
140
+
141
+ if !context
142
+ # Create the top-level context.
143
+ context = Bijou::Context.new(config)
144
+ end
145
+
146
+ if config.document_root
147
+ if !File.directory?(config.document_root)
148
+ raise "The documet_root is not a valid directory."
149
+ end
150
+ else
151
+ # NOTE: This modifies the configuration.
152
+ config.document_root = Dir.getwd
153
+ end
154
+
155
+ # We want the processor to handle container and component creation.
156
+ context.container_callback = method(:load_container_callback)
157
+ context.component_callback = method(:load_component_callback)
158
+
159
+ # Load the top-level component as a normal component.
160
+ load_component(context, path)
161
+ end
162
+
163
+ def load_component(context, path)
164
+ config = context.config
165
+
166
+ if !config.document_root
167
+ raise "A document_root must be specified"
168
+ end
169
+
170
+ path = path.strip
171
+
172
+ # TODO: Consolidate tracing to a single path.
173
+ trace "load_component #{path}"
174
+ context.trace(Bijou::Log::Info, "load_component(#{path})")
175
+
176
+ # The caller should strip the slash, but we do it here as a safeguard.
177
+ if path[0,1] =~ PathSepRE
178
+ path = path[1..-1]
179
+ end
180
+
181
+ filename = File.expand_path(path, config.document_root)
182
+ trace "filename #{filename}"
183
+
184
+ if !File.exists?(filename)
185
+ raise Bijou::FileNotFound.new(filename), "File not found: #{filename}"
186
+ end
187
+
188
+ classname = Processor.class_from_filename(ComponentPrefix, filename)
189
+ trace "classname #{classname}"
190
+
191
+ # context.trace(Bijou::Log::Info, "get_cache_path #{config.cache_root || ''}");
192
+ cachename = get_cache_path(config, path)
193
+ trace "cachename #{cachename}"
194
+
195
+ component_class = nil
196
+
197
+ if !cache_check(config, filename, cachename)
198
+ context.trace(Bijou::Log::Info, "parse and load into cache #{cachename}");
199
+ Processor.ensure_directory_exists_for_file cachename
200
+
201
+ parser = Bijou::Parser.new
202
+
203
+ trace "parse #{classname}"
204
+ source_file = File.new(filename)
205
+ parser.parse_file(classname, source_file, path)
206
+ source_file.close
207
+
208
+ class_text = parser.render(classname, filename, cachename,
209
+ config.component_base, config.require_list)
210
+
211
+ # Write the new class text to the cache file.
212
+ if parser.diagnostics.errors.length == 0 && class_text
213
+ object_file = File.open(cachename, "w")
214
+ object_file.write(class_text)
215
+ object_file.close
216
+ else
217
+ File.delete(cachename) if File.exist?(cachename)
218
+
219
+ raise Bijou::ParseError.new(filename, parser.diagnostics),
220
+ "Parse error for file '#{path}'"
221
+ end
222
+ else
223
+ begin
224
+ #
225
+ # If the class is already in memory, avoid hitting the disk.
226
+ #
227
+ trace "try read from memory #{classname}"
228
+ component_object = create_class_object(classname, context)
229
+
230
+ context.add_component(component_object)
231
+ rescue NameError
232
+ trace "not in memory"
233
+ end
234
+
235
+ if component_object
236
+ context.trace(Bijou::Log::Info, " create from memory #{cachename}");
237
+ else
238
+ context.trace(Bijou::Log::Info, " create from cache #{cachename}");
239
+
240
+ #
241
+ # Otherewise, read the class text from the cache.
242
+ #
243
+ trace "read from cache #{cachename}"
244
+
245
+ object_file = File.open(cachename, "r")
246
+ class_text = object_file.read
247
+ object_file.close
248
+ end
249
+ end
250
+
251
+ if !component_object
252
+ #
253
+ # Create component using class text, while putting the definition
254
+ # into memory.
255
+ #
256
+ trace "create_component"
257
+ begin
258
+ component_object = Processor.create_component(context,
259
+ class_text,
260
+ classname)
261
+ rescue SyntaxError
262
+ raise Bijou::EvalError.new(filename, cachename, $!.message),
263
+ "Syntax error in file '#{path}'"
264
+ end
265
+ end
266
+
267
+ trace "load finished #{path}"
268
+
269
+ # Caller will need to call context.render(args)
270
+ return context
271
+ end
272
+
273
+ def self.create_component(context, class_text, class_name)
274
+ #
275
+ # If the class is already in memory, we'll need to undefine it.
276
+ # Otherwise, the new definition would be appended to the old one.
277
+ #
278
+ module_name = "Object" # REVIEW: This is the default right now.
279
+ if Object.const_defined? module_name
280
+ module_ref = Object.const_get(module_name)
281
+
282
+ if module_ref.const_defined? class_name
283
+ module_ref.remove_const(class_name)
284
+ end
285
+ end
286
+
287
+ # REVIEW: If we can scope eval, we can use a random name.
288
+ eval(class_text)
289
+ component_class = eval(class_name)
290
+
291
+ component_object = component_class.new(context)
292
+ context.add_component(component_object)
293
+
294
+ return component_object
295
+ end
296
+
297
+ # A convenience function that parses and renders a class. It will not load
298
+ # or cache component documents, like load_component, nor will containers or
299
+ # components be automatically handled. The caller must set callbacks to
300
+ # properly handle these load events.
301
+ def self.execute(context, class_text, class_name, args={})
302
+ component_object = self.create_component(context, class_text, class_name)
303
+
304
+ context.render(args)
305
+
306
+ return context.output
307
+ end
308
+
309
+ # This helper method can be used with certain web server adapters to
310
+ # simplify the process of serving normal content types. Many web servers
311
+ # will delegate specific requests to the Bijou adapter, based on the
312
+ # request. Some servers do not delegate based on file name patterns and
313
+ # thus the adapter must disambiguate and serve such requests.
314
+ def self.handle_other(config, path_info)
315
+ content_types =
316
+ [
317
+ [ 'txt', 'text/plain' ],
318
+ [ 'htm', 'text/html' ],
319
+ [ 'html', 'text/html' ],
320
+ [ 'css', 'text/css' ],
321
+ [ 'gif', 'image/gif' ],
322
+ [ 'jpg', 'image/jpeg' ],
323
+ [ 'jpeg', 'image/jpeg' ],
324
+ [ 'png', 'image/png' ],
325
+ [ 'tif', 'image/tiff' ],
326
+ [ 'tiff', 'image/tiff' ],
327
+ [ 'bmp', 'image/x-ms-bmp' ],
328
+ ]
329
+
330
+ path = File.expand_path(path_info, config.document_root)
331
+
332
+ content_type = 'text/plain'
333
+ content_types.each {|type|
334
+ ext = type[0]
335
+
336
+ if path_info =~ /\.#{ext}$/
337
+ content_type = type[1]
338
+ break
339
+ end
340
+ }
341
+
342
+ if File.exists?(path)
343
+ file = File.new(path, "r")
344
+ response = {
345
+ 'status' => 200,
346
+ 'type' => content_type,
347
+ 'body' => file.read
348
+ }
349
+ file.close
350
+ else
351
+ response = {
352
+ 'status' => 404,
353
+ 'type' => content_type,
354
+ 'body' => ''
355
+ }
356
+ end
357
+
358
+ return response
359
+ end
360
+
361
+ def self.class_from_filename(prefix, filename)
362
+ return prefix + Digest::SHA1.hexdigest(filename.downcase).downcase
363
+ end
364
+
365
+ def self.is_cache_stale(filename, cachename)
366
+ parsefile = true
367
+
368
+ if !File.exists?(cachename)
369
+ return true # The file hasn't been cached yet.
370
+ end
371
+
372
+ cachemod = File.stat(cachename).mtime
373
+
374
+ if !File.exists?(filename)
375
+ raise "Source file not found"
376
+ end
377
+
378
+ sourcemod = File.stat(filename).mtime
379
+
380
+ if sourcemod <= cachemod
381
+ # The file has been cached and the source hasn't been touched.
382
+ return false
383
+ end
384
+
385
+ # The cached file is older than the source.
386
+ return true
387
+ end
388
+
389
+ def self.ensure_directory_exists_for_file filename
390
+ if filename !~ PathSepRE
391
+ raise "Invalid filename"
392
+ end
393
+
394
+ n = filename.rindex(PathSepRE)
395
+ path = filename[0, n]
396
+
397
+ if !File.directory?(path)
398
+ # NOTE: mkdir_p works with or without a / terminator.
399
+ FileUtils.mkdir_p(path)
400
+ end
401
+ end
402
+
403
+ end
404
+ end
@@ -0,0 +1,400 @@
1
+ #
2
+ # This file is is not part of Bijou, but is included as a helper for Ruby
3
+ # installations without stringio.
4
+ #
5
+ # Reference: http://rubyforge.org/projects/prstringio/
6
+ #
7
+ # Original comment:
8
+ #
9
+ # To load this in emergency situations when loading StringIO fails then use the following:
10
+ # begin
11
+ # require 'stringio'
12
+ # rescue LoadError
13
+ # require 'purerubystringio' #or just put this entire file in your code at this point.
14
+ # end
15
+ # If you need stringio and you know for sure it isn't going to be available then you can easily fake it
16
+ # without changing your existing code. Just add the following before the rest of your code starts.
17
+ # class StringIO < PureSybyStringIO
18
+ # end
19
+ # Any code that uses StringIO.new will now work. So will any subclass definitions. If you are only subclassing
20
+ # StringIO and you are willing to make small changes to you code you can change your class definitions to:
21
+ # class MyClass < Object.const_defined?(:StringIO) ? StringIO : PureRubyStringIO
22
+ # ...
23
+ # end
24
+ # That will automatically select which ever is avaialable at the time. Savy coders, by now, will have begin
25
+ # to consider the delegator mixin. I'll leave that exercise for you to finish. ;)
26
+ class PureRubyStringIO # :nodoc:
27
+
28
+ include Enumerable
29
+
30
+ SEEK_CUR = IO::SEEK_CUR
31
+ SEEK_END = IO::SEEK_END
32
+ SEEK_SET = IO::SEEK_SET
33
+
34
+ @@relayMethods = [:<<, :all?, :any?, :binmode, :close, :close_read, :close_write, :closed?, :closed_read?,
35
+ :closed_write?, :collect, :detect, :each, :each_byte, :each_line, :each_with_index,
36
+ :entries, :eof, :eof?, :fcntl, :fileno, :find, :find_all, :flush, :fsync, :getc, :gets,
37
+ :grep, :include?, :inject, :isatty, :length, :lineno, :lineno=, :map, :max, :member?,
38
+ :min, :partition, :path, :pid, :pos, :pos=, :print, :printf, :putc, :puts, :read,
39
+ :readchar, :readline, :readlines, :reject, :rewind, :seek, :select, :size, :sort,
40
+ :sort_by, :string, :string=, :sync, :sync=, :sysread, :syswrite, :tell, :truncate, :tty?,
41
+ :ungetc, :write, :zip]
42
+
43
+ def self.open(string="", mode="r+")
44
+ if block_given? then
45
+ sio = new(string, mode)
46
+ rc = yield(sio)
47
+ sio.close
48
+ rc
49
+ else
50
+ new(string, mode)
51
+ end
52
+ end
53
+
54
+ def <<(obj)
55
+ requireWritable
56
+ write obj
57
+ self
58
+ end
59
+
60
+ def binmode
61
+ self
62
+ end
63
+
64
+ def close
65
+ requireOpen
66
+ @sio_closed_read = true
67
+ @sio_closed_write = true
68
+ self
69
+ end
70
+
71
+ def close_read
72
+ raise IOError, "closing non-duplex IO for reading", caller if closed_read?
73
+ @sio_closed_read = true
74
+ self
75
+ end
76
+
77
+ def close_write
78
+ raise IOError, "closing non-duplex IO for writing", caller if closed_write?
79
+ @sio_closed_read = true
80
+ self
81
+ end
82
+
83
+ def closed?
84
+ closed_read? && closed_write?
85
+ end
86
+
87
+ def closed_read?
88
+ @sio_closed_read
89
+ end
90
+
91
+ def closed_write?
92
+ @sio_closed_write
93
+ end
94
+
95
+ def each(sep_string=$/, &block)
96
+ requireReadable
97
+ @sio_string.each(sep_string, &block)
98
+ @sio_pos = @sio_string.length
99
+ end
100
+
101
+ def each_byte(&block)
102
+ requireReadable
103
+ @sio_string.each_byte(&block)
104
+ @sio_pos = @sio_string.length
105
+ end
106
+
107
+ def eof
108
+ requireReadable { @sio_pos >= @sio_string.length }
109
+ end
110
+
111
+ def fcntl(integer_cmd, arg)
112
+ raise NotImplementedError, "The fcntl() function is unimplemented on this machine", caller
113
+ end
114
+
115
+ def fileno
116
+ nil
117
+ end
118
+
119
+ def flush
120
+ self
121
+ end
122
+
123
+ def fsync
124
+ 0
125
+ end
126
+
127
+ def getc
128
+ requireReadable
129
+ char = @sio_string[@sio_pos]
130
+ @sio_pos += 1 unless char.nil?
131
+ char
132
+ end
133
+
134
+ def gets(sep_string=$/)
135
+ requireReadable
136
+ @sio_lineno += 1
137
+ pstart = @sio_pos
138
+ @sio_pos = @sio_string.index(sep_string, @sio_pos) || [@sio_string.length, @sio_pos].max
139
+ @sio_string[pstart..@sio_pos]
140
+ end
141
+
142
+ def initialize(string="", mode="r+")
143
+ @sio_string = string.to_s
144
+ @sio_lineno = 0
145
+ @mode = mode
146
+ @relay = nil
147
+ case mode.delete("b")
148
+ when "r"
149
+ @sio_closed_read = false
150
+ @sio_closed_write = true
151
+ @sio_pos = 0
152
+ when "r+"
153
+ @sio_closed_read = false
154
+ @sio_closed_write = false
155
+ @sio_pos = 0
156
+ when "w"
157
+ @sio_closed_read = true
158
+ @sio_closed_write = false
159
+ @sio_pos = 0
160
+ @sio_string.replace("")
161
+ when "w+"
162
+ @sio_closed_read = false
163
+ @sio_closed_write = false
164
+ @sio_pos = 0
165
+ @sio_string.replace("")
166
+ when "a"
167
+ @sio_closed_read = true
168
+ @sio_closed_write = false
169
+ @sio_pos = @sio_string.length
170
+ when "a+"
171
+ @sio_closed_read = false
172
+ @sio_closed_write = false
173
+ @sio_pos = @sio_string.length
174
+ else
175
+ raise ArgumentError, "illegal access mode #{mode}", caller
176
+ end
177
+ end
178
+
179
+ def isatty
180
+ flase
181
+ end
182
+
183
+ def length
184
+ @sio_string.length
185
+ end
186
+
187
+ def lineno
188
+ @sio_lineno
189
+ end
190
+
191
+ def lineno=(integer)
192
+ @sio_lineno = integer
193
+ end
194
+
195
+ def path
196
+ nil
197
+ end
198
+
199
+ def pid
200
+ nil
201
+ end
202
+
203
+ def pos
204
+ @sio_pos
205
+ end
206
+
207
+ def pos=(integer)
208
+ raise Errno::EINVAL, "Invalid argument", caller if integer < 0
209
+ @sio_pos = integer
210
+ end
211
+
212
+ def print(*args)
213
+ requireWritable
214
+ args.unshift($_) if args.empty
215
+ args.each { |obj| write(obj) }
216
+ write($\) unless $\.nil?
217
+ nil
218
+ end
219
+
220
+ def printf(format_string, *args)
221
+ requireWritable
222
+ write format(format_string, *args)
223
+ nil
224
+ end
225
+
226
+ def putc(obj)
227
+ requireWritable
228
+ write(obj.is_a?(Numeric) ? sprintf("%c", obj) : obj.to_s[0..0])
229
+ obj
230
+ end
231
+
232
+ def puts(*args)
233
+ requireWritable
234
+ args.unshift("") if args.empty?
235
+ args.each { |obj|
236
+ write obj
237
+ write $/
238
+ }
239
+ nil
240
+ end
241
+
242
+ def read(length=nil, buffer=nil)
243
+ requireReadable
244
+ len = length || [@sio_string.length - @sio_pos, 0].max
245
+ raise ArgumentError, "negative length #{len} given", caller if len < 0
246
+ buffer ||= ""
247
+ pstart = @sio_pos
248
+ @sio_pos += len
249
+ buffer.replace(@sio_string[pstart..@sio_pos])
250
+ buffer.empty? && !length.nil? ? nil : buffer
251
+ end
252
+
253
+ def readchar
254
+ requireReadable
255
+ raise EOFError, "End of file reached", caller if eof?
256
+ getc
257
+ end
258
+
259
+ def readline
260
+ requireReadable
261
+ raise EOFError, "End of file reached", caller if eof?
262
+ gets
263
+ end
264
+
265
+ def readlines(sep_string=$/)
266
+ requireReadable
267
+ raise EOFError, "End of file reached", caller if eof?
268
+ rc = []
269
+ until eof
270
+ rc << gets(sep_string)
271
+ end
272
+ rc
273
+ end
274
+
275
+ def reopen(string, mode=nil)
276
+ if string.is_a?(self.class) then
277
+ raise ArgumentError, "wrong number of arguments (2 for 1)", caller if !mode.nil?
278
+ @relay = string
279
+ instance_eval(%Q{
280
+ class << self
281
+ @@relayMethods.each { |name|
282
+ define_method(name, ObjectSpace._id2ref(#{@relay.object_id}).method(("original_" + name.to_s).to_sym).to_proc)
283
+ }
284
+ end
285
+ })
286
+ else
287
+ raise ArgumentError, "wrong number of arguments (1 for 2)", caller if mode.nil?
288
+ class << self
289
+ @@relayMethods.each { |name|
290
+ alias_method(name, "original_#{name}".to_sym)
291
+ public name
292
+ }
293
+ @relay = nil
294
+ end unless @relay.nil?
295
+ @sio_string = string.to_s
296
+ @mode = mode
297
+ end
298
+ end
299
+
300
+ def rewind
301
+ @sio_pos = 0
302
+ @sio_lineno = 0
303
+ end
304
+
305
+ def seek(amount, whence=SEEK_SET)
306
+ if whence == SEEK_CUR then
307
+ offset += @sio_pos
308
+ elsif whence == SEEK_END then
309
+ offset += size
310
+ end
311
+ @sio_pos = offset
312
+ end
313
+
314
+ def string
315
+ @sio_string
316
+ end
317
+
318
+ def string=(newstring)
319
+ @sio_string = newstring
320
+ end
321
+
322
+ def sync
323
+ true
324
+ end
325
+
326
+ def sync=(boolean)
327
+ boolean
328
+ end
329
+
330
+ def sysread(length=nil, buffer=nil)
331
+ requireReadable
332
+ raise EOFError, "End of file reached", caller if eof?
333
+ read(length, buffer)
334
+ end
335
+
336
+ def syswrite(string)
337
+ requireWritable
338
+ addition = "\000" * (@sio_string.length - @sio_pos) + string.to_s
339
+ @sio_string[@sio_pos..(addition.length - 1)] = addition
340
+ @sio_pos += addition.size
341
+ addition.size
342
+ end
343
+
344
+ #In ruby 1.8.4 truncate differs from the docs in two ways.
345
+ #First, if an integer greater that the length is given then the string is expanded to the new integer
346
+ #length. As this expansion seems to contain junk characters instead of nulls I suspect this may be a
347
+ #flaw in the C code which could cause a core dump if abused/used.
348
+ #Second, the documentation states that truncate returns 0. It returns the integer instead.
349
+ #This implementation follows the documentation in the first instance as I suspect this will be fixed
350
+ #in the C code. In the second instance, it follows the actions of the C code instead of the docs.
351
+ #This was decided as it causes no immedeate harm and this ruby implentation is to be as compatable
352
+ #as possible with the C version. Should the C version change to match the docs the ruby version
353
+ #will be simple to update as well.
354
+ def truncate(integer)
355
+ requireWritable
356
+ raise Errno::EINVAL, "Invalid argument - negative length", caller if integer < 0
357
+ @sio_string[[integer, @sio_string.length].max..-1] = ""
358
+ integer
359
+ end
360
+
361
+ def ungetc(integer)
362
+ requireWritable
363
+ if @sio_pos > 0 then
364
+ @sio_pos -= 1
365
+ # BUGBUG: This wreaks havok with the existing string.
366
+ # putc(integer)
367
+ # @sio_pos -= 1
368
+ # We use this as a simple workaround since we're not modifying the stream.
369
+ @sio_string[@sio_pos] = integer
370
+ end
371
+ end
372
+
373
+ alias :each_line :each
374
+ alias :eof? :eof
375
+ alias :size :length
376
+ alias :tty? :isatty
377
+ alias :tell :pos
378
+ alias :write :syswrite
379
+
380
+ protected
381
+ @@relayMethods.each { |name|
382
+ alias_method("original_#{name}".to_sym, name)
383
+ protected "original_#{name}".to_sym
384
+ }
385
+
386
+ private
387
+
388
+ def requireReadable
389
+ raise IOError, "not opened for reading", caller[1..-1] if @sio_closed_read
390
+ end
391
+
392
+ def requireWritable
393
+ raise IOError, "not opened for writing", caller[1..-1] if @sio_closed_write
394
+ end
395
+
396
+ def requireOpen
397
+ raise IOError, "closed stream", caller[1..-1] if @sio_closed_read && @sio_closed_write
398
+ end
399
+
400
+ end