bijou 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.
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