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.
- data/ChangeLog.txt +4 -0
- data/LICENSE.txt +58 -0
- data/README.txt +48 -0
- data/Rakefile +105 -0
- data/doc/INSTALL.rdoc +260 -0
- data/doc/README.rdoc +314 -0
- data/doc/releases/bijou-0.1.0.rdoc +60 -0
- data/examples/birthday/birthday.rb +34 -0
- data/examples/holiday/holiday.rb +61 -0
- data/examples/holiday/letterhead.txt +4 -0
- data/examples/holiday/signature.txt +9 -0
- data/examples/phishing/letter.txt +29 -0
- data/examples/phishing/letterhead.txt +4 -0
- data/examples/phishing/phishing.rb +21 -0
- data/examples/phishing/signature.txt +9 -0
- data/examples/profile/profile.rb +46 -0
- data/lib/bijou.rb +15 -0
- data/lib/bijou/backend.rb +542 -0
- data/lib/bijou/cgi/adapter.rb +201 -0
- data/lib/bijou/cgi/handler.rb +5 -0
- data/lib/bijou/cgi/request.rb +37 -0
- data/lib/bijou/common.rb +12 -0
- data/lib/bijou/component.rb +108 -0
- data/lib/bijou/config.rb +60 -0
- data/lib/bijou/console/adapter.rb +167 -0
- data/lib/bijou/console/handler.rb +4 -0
- data/lib/bijou/console/request.rb +26 -0
- data/lib/bijou/context.rb +431 -0
- data/lib/bijou/diagnostics.rb +87 -0
- data/lib/bijou/errorformatter.rb +322 -0
- data/lib/bijou/exception.rb +39 -0
- data/lib/bijou/filters.rb +107 -0
- data/lib/bijou/httprequest.rb +108 -0
- data/lib/bijou/httpresponse.rb +268 -0
- data/lib/bijou/lexer.rb +513 -0
- data/lib/bijou/minicgi.rb +159 -0
- data/lib/bijou/parser.rb +1026 -0
- data/lib/bijou/processor.rb +404 -0
- data/lib/bijou/prstringio.rb +400 -0
- data/lib/bijou/webrick/adapter.rb +174 -0
- data/lib/bijou/webrick/handler.rb +32 -0
- data/lib/bijou/webrick/request.rb +45 -0
- data/script/cgi.rb +25 -0
- data/script/console.rb +7 -0
- data/script/server.rb +7 -0
- data/test/t1.cfg +5 -0
- data/test/tc_config.rb +26 -0
- data/test/tc_filter.rb +25 -0
- data/test/tc_lexer.rb +120 -0
- data/test/tc_response.rb +103 -0
- data/test/tc_ruby.rb +62 -0
- data/test/tc_stack.rb +50 -0
- 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
|