og 0.19.0 → 0.20.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 +63 -0
- data/INSTALL +4 -0
- data/README +23 -22
- data/doc/AUTHORS +3 -0
- data/doc/RELEASES +24 -1
- data/lib/og.rb +9 -8
- data/lib/og/collection.rb +4 -3
- data/lib/og/entity.rb +26 -7
- data/lib/og/manager.rb +1 -1
- data/lib/og/mixin/hierarchical.rb +9 -0
- data/lib/og/relation.rb +5 -3
- data/lib/og/relation/has_many.rb +6 -2
- data/lib/og/store.rb +3 -1
- data/lib/og/store/mysql.rb +17 -1
- data/lib/og/store/sql.rb +6 -4
- data/lib/og/store/sqlite.rb +1 -1
- data/lib/vendor/README +3 -0
- data/lib/{og/store/kirby → vendor}/kirbybase.rb +0 -0
- data/lib/vendor/mysql.rb +1117 -0
- data/lib/vendor/mysql411.rb +306 -0
- data/test/og/mixin/tc_hierarchical.rb +6 -2
- data/test/og/tc_relation.rb +6 -0
- data/test/og/tc_store.rb +21 -0
- metadata +8 -7
- data/lib/og/store/kirby/README +0 -6
- data/lib/og/store/kirby/readme.txt +0 -63
@@ -0,0 +1,306 @@
|
|
1
|
+
#
|
2
|
+
# mysq411.rb - 0.1 - Matt Mower <self@mattmower.com>
|
3
|
+
#
|
4
|
+
# The native Ruby MySQL client (mysql.rb) by Tomita Masahiro does not (yet) handle the new MySQL
|
5
|
+
# protocol introduced in MySQL 4.1.1. This protocol introduces a new authentication scheme as
|
6
|
+
# well as modifications to the client/server exchanges themselves.
|
7
|
+
#
|
8
|
+
# mysql411.rb modifies the Mysql class to add MySQL 4.1.x support. It modifies the connection
|
9
|
+
# algorithm to detect a 4.1.1 server and respond with the new authentication scheme, otherwise using
|
10
|
+
# the original one. Similarly for the changes to packet structures and field definitions, etc...
|
11
|
+
#
|
12
|
+
# It redefines serveral methods which behave differently depending upon the server context. The
|
13
|
+
# way I have implemented this is to alias the old method, create a new alternative method, and redefine
|
14
|
+
# the original method as a selector which calls the appropriate method based upon the server version.
|
15
|
+
# There may have been a neater way to do this.
|
16
|
+
#
|
17
|
+
# In general I've tried not to change the original code any more than necessary, i.e. even where I
|
18
|
+
# redefine a method I have made the smallest number of changes possible, rather than rewriting from
|
19
|
+
# scratch.
|
20
|
+
#
|
21
|
+
# *Caveat Lector* This code passes all current ActiveRecord unit tests however this is no guarantee that
|
22
|
+
# full & correct MySQL 4.1 support has been achieved.
|
23
|
+
#
|
24
|
+
|
25
|
+
require 'digest/sha1'
|
26
|
+
|
27
|
+
#
|
28
|
+
# Extend the Mysql class to work with MySQL 4.1.1+ servers. After version
|
29
|
+
# 4.1.1 the password hashing function (and some other connection details) have
|
30
|
+
# changed rendering the previous Mysql class unable to connect:
|
31
|
+
#
|
32
|
+
#
|
33
|
+
|
34
|
+
class Mysql
|
35
|
+
CLIENT_PROTOCOL_41 = 512
|
36
|
+
CLIENT_SECURE_CONNECTION = 32768
|
37
|
+
|
38
|
+
def real_connect( host=nil, user=nil, passwd=nil, db=nil, port=nil, socket=nil, flag=nil )
|
39
|
+
@server_status = SERVER_STATUS_AUTOCOMMIT
|
40
|
+
|
41
|
+
if( host == nil || host == "localhost" ) && defined? UNIXSocket
|
42
|
+
unix_socket = socket || ENV["MYSQL_UNIX_PORT"] || MYSQL_UNIX_ADDR
|
43
|
+
sock = UNIXSocket::new( unix_socket )
|
44
|
+
@host_info = Error::err( Error::CR_LOCALHOST_CONNECTION )
|
45
|
+
@unix_socket = unix_socket
|
46
|
+
else
|
47
|
+
sock = TCPSocket::new(host, port||ENV["MYSQL_TCP_PORT"]||(Socket::getservbyname("mysql","tcp") rescue MYSQL_PORT))
|
48
|
+
@host_info = sprintf Error::err(Error::CR_TCP_CONNECTION), host
|
49
|
+
end
|
50
|
+
|
51
|
+
@host = host ? host.dup : nil
|
52
|
+
sock.setsockopt Socket::SOL_SOCKET, Socket::SO_KEEPALIVE, true
|
53
|
+
@net = Net::new sock
|
54
|
+
|
55
|
+
a = read
|
56
|
+
|
57
|
+
@protocol_version = a.slice!(0)
|
58
|
+
@server_version, a = a.split(/\0/,2)
|
59
|
+
|
60
|
+
# Store the version number components for speedy comparison
|
61
|
+
version, ostag = @server_version.split( /-/, 2 )
|
62
|
+
@major_ver, @minor_ver, @revision_num = version.split( /\./ ).map { |v| v.to_i }
|
63
|
+
|
64
|
+
@thread_id, @scramble_buff = a.slice!(0,13).unpack("La8")
|
65
|
+
if a.size >= 2 then
|
66
|
+
@server_capabilities, = a.slice!(0,2).unpack("v")
|
67
|
+
end
|
68
|
+
if a.size >= 16 then
|
69
|
+
@server_language, @server_status = a.unpack("cv")
|
70
|
+
end
|
71
|
+
|
72
|
+
# Set the flags we'll send back to the server
|
73
|
+
flag = 0 if flag == nil
|
74
|
+
flag |= @client_flag | CLIENT_CAPABILITIES
|
75
|
+
flag |= CLIENT_CONNECT_WITH_DB if db
|
76
|
+
|
77
|
+
if version_meets_minimum?( 4, 1, 1 )
|
78
|
+
# In 4.1.1+ the seed comes in two parts which must be combined
|
79
|
+
a.slice!( 0, 16 )
|
80
|
+
seed_part_2 = a.slice!( 0, 12 );
|
81
|
+
@scramble_buff << seed_part_2
|
82
|
+
|
83
|
+
flag |= CLIENT_FOUND_ROWS
|
84
|
+
flag |= CLIENT_PROTOCOL_41
|
85
|
+
flag |= CLIENT_SECURE_CONNECTION if @server_capabilities & CLIENT_SECURE_CONNECTION;
|
86
|
+
|
87
|
+
if db && @server_capabilities & CLIENT_CONNECT_WITH_DB != 0
|
88
|
+
@db = db.dup
|
89
|
+
end
|
90
|
+
|
91
|
+
scrambled_password = scramble411( passwd, @scramble_buff, @protocol_version==9 )
|
92
|
+
data = make_client_auth_packet_41( flag, user, scrambled_password, db )
|
93
|
+
else
|
94
|
+
scrambled_password = scramble( passwd, @scramble_buff, @protocol_version == 9 )
|
95
|
+
data = Net::int2str(flag)+Net::int3str(@max_allowed_packet)+(user||"")+"\0"+scrambled_password
|
96
|
+
if db and @server_capabilities & CLIENT_CONNECT_WITH_DB != 0 then
|
97
|
+
data << "\0"+db
|
98
|
+
@db = db.dup
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
write data
|
103
|
+
read
|
104
|
+
self
|
105
|
+
end
|
106
|
+
alias :connect :real_connect
|
107
|
+
|
108
|
+
# Pack the authentication information into depending upon whether an initial database has
|
109
|
+
# been specified
|
110
|
+
def make_client_auth_packet_41( flag, user, password, db )
|
111
|
+
if db && @server_capabilities & CLIENT_CONNECT_WITH_DB != 0
|
112
|
+
template = "VVcx23a#{user.size+1}cA#{password.size}a#{db.size+1}"
|
113
|
+
else
|
114
|
+
template = "VVcx23a#{user.size+1}cA#{password.size}x"
|
115
|
+
end
|
116
|
+
|
117
|
+
[ flag, @max_allowed_packet, @server_language, user, password.size, password, db ].pack( template )
|
118
|
+
end
|
119
|
+
|
120
|
+
def version_meets_minimum?( major, minor, revision )
|
121
|
+
@major_ver >= major && @minor_ver >= minor && @revision_num >= revision
|
122
|
+
end
|
123
|
+
|
124
|
+
# SERVER: public_seed=create_random_string()
|
125
|
+
# send(public_seed)
|
126
|
+
#
|
127
|
+
# CLIENT: recv(public_seed)
|
128
|
+
# hash_stage1=sha1("password")
|
129
|
+
# hash_stage2=sha1(hash_stage1)
|
130
|
+
# reply=xor(hash_stage1, sha1(public_seed,hash_stage2)
|
131
|
+
#
|
132
|
+
# #this three steps are done in scramble()
|
133
|
+
#
|
134
|
+
# send(reply)
|
135
|
+
#
|
136
|
+
#
|
137
|
+
# SERVER: recv(reply)
|
138
|
+
# hash_stage1=xor(reply, sha1(public_seed,hash_stage2))
|
139
|
+
# candidate_hash2=sha1(hash_stage1)
|
140
|
+
# check(candidate_hash2==hash_stage2)
|
141
|
+
def scramble411( password, seed, old_ver )
|
142
|
+
return "" if password == nil or password == ""
|
143
|
+
raise "old version password is not implemented" if old_ver
|
144
|
+
|
145
|
+
# print "Seed Bytes = "
|
146
|
+
# seed.each_byte { |b| print "0x#{b.to_s( 16 )}, " }
|
147
|
+
# puts
|
148
|
+
|
149
|
+
stage1 = Digest::SHA1.digest( password )
|
150
|
+
stage2 = Digest::SHA1.digest( stage1 )
|
151
|
+
|
152
|
+
dgst = Digest::SHA1.new
|
153
|
+
dgst << seed
|
154
|
+
dgst << stage2
|
155
|
+
stage3 = dgst.digest
|
156
|
+
|
157
|
+
# stage1.zip( stage3 ).map { |a, b| (a ^ b).chr }.join
|
158
|
+
scrambled = ( 0 ... stage3.size ).map { |i| stage3[i] ^ stage1[i] }
|
159
|
+
scrambled = scrambled.map { |x| x.chr }
|
160
|
+
scrambled.join
|
161
|
+
end
|
162
|
+
|
163
|
+
def change_user(user="", passwd="", db="")
|
164
|
+
scrambled_password = version_meets_minimum?( 4, 1, 1 ) ? scramble411( passwd, @scramble_buff, @protocol_version==9 ) : scramble( passwd, @scramble_buff, @protocol_version==9 )
|
165
|
+
data = user+"\0"+scrambled_password+"\0"+db
|
166
|
+
command COM_CHANGE_USER, data
|
167
|
+
@user = user
|
168
|
+
@passwd = passwd
|
169
|
+
@db = db
|
170
|
+
end
|
171
|
+
|
172
|
+
#
|
173
|
+
# The 4.1 protocol changed the length of the END packet
|
174
|
+
#
|
175
|
+
alias_method :old_read_one_row, :read_one_row
|
176
|
+
|
177
|
+
def read_one_row( field_count )
|
178
|
+
if version_meets_minimum?( 4, 1, 1 )
|
179
|
+
read_one_row_41( field_count )
|
180
|
+
else
|
181
|
+
old_read_one_row( field_count )
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def read_one_row_41( field_count )
|
186
|
+
data = read
|
187
|
+
return if data[0] == 254 and data.length < 9
|
188
|
+
rec = []
|
189
|
+
field_count.times do
|
190
|
+
len = get_length data
|
191
|
+
if len == nil then
|
192
|
+
rec << len
|
193
|
+
else
|
194
|
+
rec << data.slice!(0,len)
|
195
|
+
end
|
196
|
+
end
|
197
|
+
rec
|
198
|
+
end
|
199
|
+
|
200
|
+
#
|
201
|
+
# The 4.1 protocol changed the length of the END packet
|
202
|
+
#
|
203
|
+
alias_method :old_skip_result, :skip_result
|
204
|
+
|
205
|
+
def skip_result
|
206
|
+
if version_meets_minimum?( 4, 1, 1 )
|
207
|
+
skip_result_41
|
208
|
+
else
|
209
|
+
old_skip_result
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
def skip_result_41()
|
214
|
+
if @status == :STATUS_USE_RESULT then
|
215
|
+
loop do
|
216
|
+
data = read
|
217
|
+
break if data[0] == 254 and data.length == 1
|
218
|
+
end
|
219
|
+
@status = :STATUS_READY
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
# The field description structure is changed for the 4.1 protocol passing
|
224
|
+
# more data and a different packing form. NOTE: The 4.1 protocol now passes
|
225
|
+
# back a "catalog" name for each field which is a new feature. Since AR has
|
226
|
+
# nowhere to put it I'm throwing it away. Possibly this is not the best
|
227
|
+
# idea?
|
228
|
+
#
|
229
|
+
alias_method :old_unpack_fields, :unpack_fields
|
230
|
+
|
231
|
+
def unpack_fields( data, long_flag_protocol )
|
232
|
+
if version_meets_minimum?( 4, 1, 1 )
|
233
|
+
unpack_fields_41( data, long_flag_protocol )
|
234
|
+
else
|
235
|
+
old_unpack_fields( data, long_flag_protocol )
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def unpack_fields_41( data, long_flag_protocol )
|
240
|
+
ret = []
|
241
|
+
|
242
|
+
data.each do |f|
|
243
|
+
catalog_name = f[0]
|
244
|
+
database_name = f[1]
|
245
|
+
table_name_alias = f[2]
|
246
|
+
table_name = f[3]
|
247
|
+
column_name_alias = f[4]
|
248
|
+
column_name = f[5]
|
249
|
+
|
250
|
+
charset = f[6][0] + f[6][1]*256
|
251
|
+
length = f[6][2] + f[6][3]*256 + f[6][4]*256*256 + f[6][5]*256*256*256
|
252
|
+
type = f[6][6]
|
253
|
+
flags = f[6][7] + f[6][8]*256
|
254
|
+
decimals = f[6][9]
|
255
|
+
def_value = f[7]
|
256
|
+
max_length = 0
|
257
|
+
|
258
|
+
ret << Field::new(table_name, table_name, column_name_alias, length, type, flags, decimals, def_value, max_length)
|
259
|
+
end
|
260
|
+
ret
|
261
|
+
end
|
262
|
+
|
263
|
+
# In this instance the read_query_result method in mysql is bound to read 5 field parameters which
|
264
|
+
# is expanded to 7 in the 4.1 protocol. So in this case we redefine this entire method in order
|
265
|
+
# to write "read_rows 7" instead of "read_rows 5"!
|
266
|
+
#
|
267
|
+
alias_method :old_read_query_result, :read_query_result
|
268
|
+
|
269
|
+
def read_query_result
|
270
|
+
if version_meets_minimum?( 4, 1, 1 )
|
271
|
+
read_query_result_41
|
272
|
+
else
|
273
|
+
old_read_query_result
|
274
|
+
end
|
275
|
+
end
|
276
|
+
|
277
|
+
def read_query_result_41
|
278
|
+
data = read
|
279
|
+
@field_count = get_length(data)
|
280
|
+
if @field_count == nil then # LOAD DATA LOCAL INFILE
|
281
|
+
File::open(data) do |f|
|
282
|
+
write f.read
|
283
|
+
end
|
284
|
+
write "" # mark EOF
|
285
|
+
data = read
|
286
|
+
@field_count = get_length(data)
|
287
|
+
end
|
288
|
+
if @field_count == 0 then
|
289
|
+
@affected_rows = get_length(data, true)
|
290
|
+
@insert_id = get_length(data, true)
|
291
|
+
if @server_capabilities & CLIENT_TRANSACTIONS != 0 then
|
292
|
+
a = data.slice!(0,2)
|
293
|
+
@server_status = a[0]+a[1]*256
|
294
|
+
end
|
295
|
+
if data.size > 0 and get_length(data) then
|
296
|
+
@info = data
|
297
|
+
end
|
298
|
+
else
|
299
|
+
@extra_info = get_length(data, true)
|
300
|
+
fields = read_rows 7
|
301
|
+
@fields = unpack_fields(fields, @server_capabilities & CLIENT_LONG_FLAG != 0)
|
302
|
+
@status = :STATUS_GET_RESULT
|
303
|
+
end
|
304
|
+
self
|
305
|
+
end
|
306
|
+
end
|
@@ -7,9 +7,9 @@ require 'og'
|
|
7
7
|
require 'og/mixin/hierarchical'
|
8
8
|
|
9
9
|
$og = Og.setup(
|
10
|
-
:store => '
|
10
|
+
:store => 'mysql',
|
11
11
|
:name => 'test',
|
12
|
-
:user => '
|
12
|
+
:user => 'root',
|
13
13
|
:password => 'navelrulez',
|
14
14
|
:destroy => true
|
15
15
|
)
|
@@ -71,6 +71,10 @@ class TC_OgHierarchical < Test::Unit::TestCase # :nodoc: all
|
|
71
71
|
assert_equal 6, c1.full_comments.size
|
72
72
|
assert_equal 5, c1.comments.size
|
73
73
|
assert_equal 2, c1.direct_comments.size
|
74
|
+
|
75
|
+
c8.reload
|
76
|
+
|
77
|
+
assert_equal 'root', c8.parent.body
|
74
78
|
end
|
75
79
|
|
76
80
|
end
|
data/test/og/tc_relation.rb
CHANGED
@@ -28,6 +28,12 @@ class TestCaseOgRelation < Test::Unit::TestCase # :nodoc: all
|
|
28
28
|
rel = User.relation(:articles)
|
29
29
|
rel.resolve_target
|
30
30
|
assert_equal TestCaseOgRelation::Article, rel.target_class
|
31
|
+
|
32
|
+
# bug: test the no belongs_to case in Article
|
33
|
+
|
34
|
+
og = Og.setup(:store => :memory, :name => 'test')
|
35
|
+
og.manage_classes
|
36
|
+
|
31
37
|
end
|
32
38
|
end
|
33
39
|
|
data/test/og/tc_store.rb
CHANGED
@@ -48,12 +48,19 @@ class TCOgStore < Test::Unit::TestCase # :nodoc: all
|
|
48
48
|
belongs_to :article, Article
|
49
49
|
belongs_to User
|
50
50
|
|
51
|
+
order 'hits ASC'
|
52
|
+
|
51
53
|
def initialize(body = nil, user = nil)
|
52
54
|
@body = body
|
53
55
|
@user = user
|
54
56
|
@hits = 0
|
55
57
|
end
|
56
58
|
end
|
59
|
+
|
60
|
+
class Bugger
|
61
|
+
property :name, String
|
62
|
+
many_to_many Bugger
|
63
|
+
end
|
57
64
|
|
58
65
|
def setup
|
59
66
|
@og = nil
|
@@ -262,6 +269,11 @@ class TCOgStore < Test::Unit::TestCase # :nodoc: all
|
|
262
269
|
|
263
270
|
assert_equal 4, a4.comments(:reload => true).size
|
264
271
|
|
272
|
+
assert_equal 2, a4.comments(:limit => 2, :reload => true).size
|
273
|
+
|
274
|
+
# bug:
|
275
|
+
assert_equal 4, a4.comments(:reload => true).size
|
276
|
+
|
265
277
|
a4.comments.delete(c1)
|
266
278
|
assert_equal 3, a4.comments.size
|
267
279
|
|
@@ -311,6 +323,15 @@ class TCOgStore < Test::Unit::TestCase # :nodoc: all
|
|
311
323
|
|
312
324
|
c = Category.find_by_title('News')
|
313
325
|
assert_equal 1, c.articles.size
|
326
|
+
|
327
|
+
# bug: self join bug.
|
328
|
+
|
329
|
+
b1 = Bugger.create
|
330
|
+
b2 = Bugger.create
|
331
|
+
|
332
|
+
b1.buggers << b2
|
333
|
+
|
334
|
+
assert b1.buggers.first
|
314
335
|
end
|
315
336
|
|
316
337
|
def conversions_test
|
metadata
CHANGED
@@ -3,8 +3,8 @@ rubygems_version: 0.8.10
|
|
3
3
|
specification_version: 1
|
4
4
|
name: og
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2005-
|
6
|
+
version: 0.20.0
|
7
|
+
date: 2005-07-12
|
8
8
|
summary: Og (ObjectGraph)
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -47,6 +47,7 @@ files:
|
|
47
47
|
- doc/AUTHORS
|
48
48
|
- lib/og
|
49
49
|
- lib/og.rb
|
50
|
+
- lib/vendor
|
50
51
|
- lib/og/collection.rb
|
51
52
|
- lib/og/entity.rb
|
52
53
|
- lib/og/relation.rb
|
@@ -78,10 +79,10 @@ files:
|
|
78
79
|
- lib/og/store/sqlite.rb
|
79
80
|
- lib/og/store/kirby.rb
|
80
81
|
- lib/og/store/memory.rb
|
81
|
-
- lib/
|
82
|
-
- lib/
|
83
|
-
- lib/
|
84
|
-
- lib/
|
82
|
+
- lib/vendor/mysql411.rb
|
83
|
+
- lib/vendor/mysql.rb
|
84
|
+
- lib/vendor/kirbybase.rb
|
85
|
+
- lib/vendor/README
|
85
86
|
- test/og
|
86
87
|
- test/og/store
|
87
88
|
- test/og/mixin
|
@@ -120,7 +121,7 @@ dependencies:
|
|
120
121
|
-
|
121
122
|
- "="
|
122
123
|
- !ruby/object:Gem::Version
|
123
|
-
version: 0.
|
124
|
+
version: 0.20.0
|
124
125
|
version:
|
125
126
|
- !ruby/object:Gem::Dependency
|
126
127
|
name: facets
|
data/lib/og/store/kirby/README
DELETED
@@ -1,63 +0,0 @@
|
|
1
|
-
KirbyBase 2.2
|
2
|
-
|
3
|
-
A small, plain-text, dbms written in Ruby. It can be used either embedded
|
4
|
-
or client/server. Version 2 is a complete re-write with a completely new
|
5
|
-
interface. It is NOT backwards compatible. If you need backwards
|
6
|
-
compatibility, please use version 1.6.
|
7
|
-
|
8
|
-
|
9
|
-
*Installation:
|
10
|
-
|
11
|
-
Unpack the file you downloaded. Execute "ruby install.rb" or simply make
|
12
|
-
sure kirbybase.rb is somewhere in your Ruby library path.
|
13
|
-
|
14
|
-
|
15
|
-
*Documentation:
|
16
|
-
|
17
|
-
Documentation is in manual.html. Also, RDoc generated documentation is in
|
18
|
-
the doc directory.
|
19
|
-
|
20
|
-
See kbtest.rb for examples of how to use KirbyBase.
|
21
|
-
|
22
|
-
|
23
|
-
*Manifest:
|
24
|
-
|
25
|
-
readme.txt - this file
|
26
|
-
install.rb - install script
|
27
|
-
changes.txt - history of changes.
|
28
|
-
manual.html - documentation
|
29
|
-
kirbybase.rb - dbms library
|
30
|
-
kbserver.rb - multi-threaded database server script.
|
31
|
-
kbtest.rb - test script with examples.
|
32
|
-
record_class_test.rb - script showing how to have KB return class instances.
|
33
|
-
csv_import_test.rb - script showing how to have KB import a csv file.
|
34
|
-
plane.csv - sample csv file used in csv_import_test.rb
|
35
|
-
doc directory - RDoc generated documentation in html format.
|
36
|
-
|
37
|
-
|
38
|
-
*License:
|
39
|
-
|
40
|
-
KirbyBase is distributed under the same license as Ruby.
|
41
|
-
|
42
|
-
|
43
|
-
*Warranty:
|
44
|
-
|
45
|
-
I should probably put something more legalese here but let me just say:
|
46
|
-
|
47
|
-
KirbyBase carries no warranty! Use at your own risk. If it eats your
|
48
|
-
data, please don't come after me. :)
|
49
|
-
|
50
|
-
|
51
|
-
That being said, please send any bug reports, suggestions, ideas,
|
52
|
-
improvements, to:
|
53
|
-
|
54
|
-
jcribbs@twmi.rr.com
|
55
|
-
|
56
|
-
You can find more info about KirbyBase at:
|
57
|
-
|
58
|
-
http://www.netpromi.com/kirbybase.html
|
59
|
-
|
60
|
-
|
61
|
-
Thanks for trying KirbyBase.
|
62
|
-
|
63
|
-
Jamey Cribbs
|