bijou 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|