palm 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1 @@
1
+ 0.0.1 - Initial Release
File without changes
@@ -0,0 +1,20 @@
1
+ CHANGELOG.txt
2
+ History.txt
3
+ Manifest.txt
4
+ README.txt
5
+ Rakefile
6
+ lib/palm.rb
7
+ lib/palm/palm_record.rb
8
+ lib/palm/palm_support.rb
9
+ lib/palm/pdb.rb
10
+ lib/palm/raw_record.rb
11
+ lib/palm/version.rb
12
+ lib/palm/waba_db.rb
13
+ lib/palm/waba_io.rb
14
+ lib/palm/waba_record.rb
15
+ setup.rb
16
+ test/HovData.pdb
17
+ test/pdb_test.rb
18
+ test/test_helper.rb
19
+ test/waba_db_test.rb
20
+ test/waba_records_test.rb
@@ -0,0 +1,41 @@
1
+ = Palm
2
+ The palm library is a pure ruby library for reading and writing Palm PDB
3
+ databases. This library is based off of Andrew Arensburger's pdb.pm.
4
+
5
+ Adam Sanderson, 2006
6
+ netghost@gmail.com
7
+
8
+ = Usage
9
+ Here is a sample that reads through and prints some metadata.
10
+ pdb = Palm::PDB.new('palm_db.pdb')
11
+ puts pdb.name
12
+ puts "Creator #{pdb.creator} / Type #{pdb.type}"
13
+ puts "There are #{pdb.data.length} records."
14
+
15
+ Here is an example of adding and removing records:
16
+ pdb = Palm::PDB.new('palm_db.pdb')
17
+ #Remove the last record
18
+ last_record = pdb.data.pop
19
+ #Append a new fake record
20
+ pdb.data << Palm::RawRecord.new("This would be binary data")
21
+ pdb.write_file('new_palm_db.pdb')
22
+
23
+ = Extending
24
+ The base Palm::PDB will read and write raw PDB files. Their binary data is
25
+ maintained in each record. This is probably not very useful for most cases, but
26
+ will allow access to all the common metadata.
27
+
28
+ To create a more specific implementation, you should override pack_entry and
29
+ unpack_entry to handle specific record types. See Palm::WabaDB for an example
30
+ implementation supporting Waba format PDBs.
31
+
32
+ = Plans
33
+ I am not entirely sold on the current API, a lot of the structure of the code
34
+ is based on Andrew Arensburger's perl code, which doesn't make for great ruby
35
+ code. Where possible I have tried to make the code simpler and more
36
+ rubalicious, but some perly bits show through. So the API might change a
37
+ little, I would really appreciate some input.
38
+
39
+ I personally have no need for reading and writing the Palm Todo Lists,
40
+ Calendars, Notes, and so forth, however if there is sufficient interest, it
41
+ might be fun to add.
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/clean'
4
+ require 'rake/testtask'
5
+ require 'rake/packagetask'
6
+ require 'rake/gempackagetask'
7
+ require 'rake/rdoctask'
8
+ require 'rake/contrib/rubyforgepublisher'
9
+ require 'fileutils'
10
+ require 'hoe'
11
+ include FileUtils
12
+ require File.join(File.dirname(__FILE__), 'lib', 'palm', 'version')
13
+
14
+ AUTHOR = "Adam Sanderson"
15
+ EMAIL = "netghost@u.washington.edu"
16
+ DESCRIPTION = "Pure Ruby library for reading and writing Palm PDB databases."
17
+ GEM_NAME = "palm"
18
+ RUBYFORGE_PROJECT = "palm"
19
+ HOMEPATH = "http://#{RUBYFORGE_PROJECT}.rubyforge.org"
20
+ RELEASE_TYPES = %w( gem tar ) # can use: gem, tar, zip
21
+
22
+
23
+ NAME = "palm"
24
+ REV = nil # UNCOMMENT IF REQUIRED: File.read(".svn/entries")[/committed-rev="(d+)"/, 1] rescue nil
25
+ VERS = ENV['VERSION'] || (Palm::VERSION::STRING + (REV ? ".#{REV}" : ""))
26
+ CLEAN.include ['**/.*.sw?', '*.gem', '.config']
27
+ RDOC_OPTS = ['--quiet', '--title', "palm documentation",
28
+ "--opname", "index.html",
29
+ "--line-numbers",
30
+ "--main", "README",
31
+ "--inline-source"]
32
+
33
+ # Generate all the Rake tasks
34
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
35
+ hoe = Hoe.new(GEM_NAME, VERS) do |p|
36
+ p.author = AUTHOR
37
+ p.description = DESCRIPTION
38
+ p.email = EMAIL
39
+ p.summary = DESCRIPTION
40
+ p.url = HOMEPATH
41
+ p.rubyforge_name = RUBYFORGE_PROJECT if RUBYFORGE_PROJECT
42
+ p.test_globs = ["test/**/*_test.rb"]
43
+ p.clean_globs = CLEAN #An array of file patterns to delete on clean.
44
+
45
+ # == Optional
46
+ #p.changes - A description of the release's latest changes.
47
+ #p.extra_deps - An array of rubygem dependencies.
48
+ #p.spec_extras - A hash of extra values to set in the gemspec.
49
+ end
50
+
@@ -0,0 +1 @@
1
+ Dir[File.join(File.dirname(__FILE__), 'palm/**/*.rb')].sort.each { |lib| require lib }
@@ -0,0 +1,62 @@
1
+ module Palm
2
+ # Base class for all Palm::PDB Records. This stores the basic metadata for each
3
+ # record. Subclasses should extend this provide a useful interface for accessing
4
+ # specific record types.
5
+ class Record
6
+ RECORD_ATTRIBUTE_CODES = {
7
+ :expunged => 0x80,
8
+ :dirty => 0x40,
9
+ :deleted => 0x20,
10
+ :private => 0x10
11
+ }
12
+
13
+ attr_accessor :expunged, :dirty, :deleted, :private, :archive
14
+ attr_accessor :record_id, :category
15
+
16
+ def initialize
17
+ @category = 0
18
+ @record_id = 0
19
+ @dirty = true
20
+ end
21
+
22
+ def packed_attributes
23
+ encoded = 0
24
+ if @expunged or @deleted
25
+ encoded |= 0x08 if @archive
26
+ else
27
+ encoded = @category & 0x0f
28
+ end
29
+
30
+ RECORD_ATTRIBUTE_CODES.each do |name,code|
31
+ encoded |= code if send(name)
32
+ end
33
+
34
+ encoded
35
+ end
36
+
37
+ def packed_attributes=(value)
38
+ RECORD_ATTRIBUTE_CODES.each do |key,code|
39
+ self.send("#{key}=", (value & code) > 0)
40
+ end
41
+ if (value & 0xa0) == 0
42
+ @category = (value & 0x0f)
43
+ else
44
+ @archive = (value & 0x08) > 0
45
+ end
46
+ end
47
+
48
+ end
49
+
50
+ # Base class for all Palm::PDB Resources. This stores the basic metadata for each
51
+ # record. Subclasses should extend this provide a useful interface for accessing
52
+ # specific record types.
53
+ class Resource
54
+ attr_accessor :record_type, :record_id
55
+ def intialize
56
+ @record_type = "\0\0\0\0"
57
+ @record_id = 0
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,12 @@
1
+ # Class extensions for making palm data easier to work with
2
+ class Time
3
+ EPOC_1904 = 2082844800 # Difference between Palm's epoch
4
+
5
+ def to_palm_seconds
6
+ to_i + EPOC_1904
7
+ end
8
+
9
+ def self.at_palm_seconds(seconds)
10
+ at(seconds - EPOC_1904)
11
+ end
12
+ end
@@ -0,0 +1,370 @@
1
+ require 'enumerator'
2
+
3
+ # This is a port of Andrew Arensburger's Perl Palm database module
4
+ # I have attempted to make the code as ruby friendly as possible, while still working ;)
5
+ # Perl code does not good ruby api design make, thus, I'll be moving stuff
6
+ # around to make something more natural soon. (Read API changes ahead)
7
+ #
8
+ # See the README for some more goodies.
9
+ #
10
+ # It is currently only somewhat tested, so I would love some more feedback
11
+ # Adam Sanderson, 2006
12
+ # netghost@gmail.com
13
+
14
+ module Palm
15
+ # Internal structure for storing information about data entries
16
+ DataBlock = Struct.new( :offset, :record_length)
17
+ # Internal structure for recording index information
18
+ RecordIndex = Struct.new( :record_id,:packed_attributes, :offset, :record_length)
19
+ # Internal structure for recording index information
20
+ ResourceIndex = Struct.new( :record_id,:record_type, :offset, :record_length)
21
+
22
+ # PDB handles reading and writing raw Palm PDB records and resources.
23
+ # For most cases, users will probably want to extend this class class, overriding
24
+ # pack_entry and unpack_entry to support their record types.
25
+ #
26
+ # Records are simply stored as an array in +data+, so polish up on your
27
+ # enumerable tricks. The +created_at+, +modified_at+, and +backed_up_at+
28
+ # attributes are all stored as Times. Note that +modified_at+ is not
29
+ # automatically updated.
30
+ class PDB
31
+ attr_accessor :name, :attributes, :version
32
+ attr_accessor :created_at, :modified_at, :backed_up_at
33
+ attr_accessor :modnum, :type, :creator
34
+ attr_accessor :unique_id_seed
35
+ attr_accessor :data
36
+
37
+ HEADER_LENGTH = 32+2+2+(9*4) # Size of database header
38
+ RECORD_INDEX_HEADER_LEN = 6 # Size of record index header
39
+ INDEX_RECORD_LENGTH = 8 # Length of record index entry
40
+ INDEX_RESOURCE_LENGTH = 10 # Length of resource index entry
41
+
42
+ ATTRIBUTE_CODES = {
43
+ "resource" => 0x0001,
44
+ "read-only" => 0x0002,
45
+ "AppInfo dirty" => 0x0004,
46
+ "backup" => 0x0008,
47
+ "OK newer" => 0x0010,
48
+ "reset" => 0x0020,
49
+ "launchable" => 0x0200,
50
+ "open" => 0x8000,
51
+
52
+ # PalmOS 5.0 attribute names
53
+ "ResDB" => 0x0001,
54
+ "ReadOnly" => 0x0002,
55
+ "AppInfoDirty" => 0x0004,
56
+ "Backup" => 0x0008,
57
+ "OKToInstallNewer" => 0x0010,
58
+ "ResetAfterInstall"=> 0x0020,
59
+ "LaunchableData" => 0x0200,
60
+ "Recyclable" => 0x0400,
61
+ "Bundle" => 0x0800,
62
+ "Open" => 0x8000,
63
+ }
64
+
65
+ # Creates a new PDB. If +from+ is passed a String, a file will be
66
+ # loaded from that path (see +load_file+). If a IO object is passed in,
67
+ # then it will be used to load the palm data (see +load+).
68
+ def initialize(from = nil)
69
+ @attributes = {}
70
+ @data = []
71
+ @appinfo_block = nil
72
+ @sort_block = nil
73
+
74
+ case from
75
+ when NilClass
76
+ now = Time.now
77
+ @created_at = now
78
+ @modified_at = now
79
+ @version = 0
80
+ @modnum = 0
81
+ @type = "\0\0\0\0"
82
+ @creator = "\0\0\0\0"
83
+ @unique_id_seed = 0
84
+ when String
85
+ load(open(from))
86
+ when IO
87
+ load(from)
88
+ else
89
+ raise ArgumentError.new("Unknown value to load from #{from.inspect}. Use a String or IO object.")
90
+ end
91
+ end
92
+
93
+ # Returns true if the PDB is a set of resources, false if it is a set of records
94
+ def resource?
95
+ @attributes['resource'] || @attributes['ResDB']
96
+ end
97
+
98
+ # Loads the PDB from a file path
99
+ def load_file(path)
100
+ open path, "r" do |io|
101
+ load io
102
+ end
103
+ end
104
+
105
+ # Loads the PDB from the given IO source.
106
+ def load(io)
107
+ # Set to binary mode for windows environment
108
+ io.binmode if io.respond_to? :binmode
109
+
110
+ start_postion = io.pos
111
+ io.seek(0, IO::SEEK_END)
112
+ io_size = io.pos
113
+ io.seek(start_postion)
114
+
115
+ appinfo_offset, sort_offset = unpack_header(io.read(HEADER_LENGTH))
116
+
117
+ # parse the record index
118
+ record_index = io.read(RECORD_INDEX_HEADER_LEN)
119
+ next_index, record_count = record_index.unpack("N n")
120
+
121
+ # load the indexes, gather information about offsets and
122
+ # record lengths
123
+ indexes = nil
124
+ if resource?
125
+ indexes = load_resource_index(io, next_index, record_count)
126
+ else
127
+ indexes = load_record_index(io, next_index, record_count)
128
+ end
129
+ # Add the final offset as a Datablock for the end of the file
130
+ indexes << DataBlock.new(io_size, 0)
131
+ # Fill in the lengths for each of these index entries
132
+ indexes.each_cons(2){|starts, ends| starts.record_length = ends.offset - starts.offset }
133
+ # Calculate where the data starts (or end of file if empty)
134
+ data_offset = indexes.first.offset
135
+
136
+ # Pop the last entry back off. We pushed it on make it easier to calculate the lengths
137
+ # of each entry.
138
+ indexes.pop
139
+
140
+ # Load optional chunks
141
+ load_appinfo_block(io, appinfo_offset, sort_offset, data_offset) if appinfo_offset > 0
142
+ load_sort_block(io, sort_offset, data_offset) if sort_offset > 0
143
+
144
+ # Load data
145
+ load_data(io, indexes)
146
+ io.close
147
+ end
148
+
149
+ protected
150
+ # Custom PDB formats must overide this to support their record format.
151
+ # The default implementation returns
152
+ # RawRecord or RawResource classes depending on the PDB's metadata.
153
+ def unpack_entry(byte_string)
154
+ entry = resource? ? RawResource.new : RawRecord.new
155
+ entry.data = byte_string # Duck typing rules! :)
156
+ entry
157
+ end
158
+
159
+ # Parses the header, returning the app_info_offset and sort_offset
160
+ def unpack_header(header)
161
+ @name, bin_attributes, @version, @created_at, @modified_at, @backed_up_at,
162
+ @modnum, appinfo_offset, sort_offset, @type, @creator,
163
+ @unique_id_seed = header.unpack("a32 n n N N N N N N a4 a4 N")
164
+
165
+ # Clean up some of the input:
166
+ @name.rstrip! # Get rid of null characters at the end of the name
167
+
168
+ ATTRIBUTE_CODES.each do |key,code|
169
+ @attributes[key] = (bin_attributes & code) > 0
170
+ end
171
+
172
+ @created_at = Time.at_palm_seconds @created_at
173
+ @modified_at = Time.at_palm_seconds @modified_at
174
+ @backed_up_at = Time.at_palm_seconds @backed_up_at
175
+ [appinfo_offset, sort_offset]
176
+ end
177
+
178
+ def load_resource_index(io, next_index, record_count)
179
+ (0...record_count).map do |i|
180
+ index = ResourceIndex.new
181
+ resource_index = io.read(INDEX_RESOURCE_LENGTH)
182
+ index.record_type, index.record_id, index.offset = resource_index.unpack "a4 n N"
183
+ index
184
+ end
185
+ end
186
+
187
+ def load_record_index(io, next_index, record_count)
188
+ last_offset = 0
189
+ (0...record_count).map do |i|
190
+ index = RecordIndex.new
191
+ record_index = io.read(INDEX_RECORD_LENGTH)
192
+ offset, packed_attributes, id_a,id_b,id_c = record_index.unpack "N C C3"
193
+ # The ID is a 3 byte number... of course ;)
194
+ record_id = (id_a << 16) | (id_b << 8) | id_c
195
+
196
+ index.packed_attributes = packed_attributes
197
+ index.record_id = record_id
198
+ index.offset = offset
199
+ index
200
+ end
201
+ end
202
+
203
+ def load_appinfo_block(io, appinfo_offset, sort_offset, data_offset)
204
+ if io.pos > appinfo_offset
205
+ raise IOError.new("Bad appinfo_offset (#{appinfo_offset}), while at #{io.pos} of #{io.inspect}.")
206
+ end
207
+ io.seek(appinfo_offset)
208
+
209
+ # Read either to the sort offset, or to the data offset
210
+ length = (sort_offset > 0 ? sort_offset : data_offset) - appinfo_offset
211
+ unpack_appinfo_block(io.read(length))
212
+ end
213
+
214
+ def load_sort_block(io, sort_offset, data_offset)
215
+ if io.pos > sort_offset
216
+ raise IOError.new("Bad sort_offset (#{sort_offset}), while at #{io.pos} of #{io.inspect}.")
217
+ end
218
+
219
+ io.seek sort_offset
220
+ # Read to the data offset
221
+ length = data_offset - sort_offset
222
+ unpack_sort_block(io.read(length))
223
+ end
224
+
225
+ def load_data(io, indexes)
226
+ @data = indexes.map do |index|
227
+ if io.pos > index.offset
228
+ raise IOError.new("Bad index offset (#{index.offset}), while at #{io.pos} of #{io.inspect}.")
229
+ end
230
+ io.seek index.offset
231
+
232
+ #Create a record
233
+ byte_string = io.read(index.record_length)
234
+ entry = unpack_entry(byte_string)
235
+
236
+ # Fill in information from the header
237
+ entry.record_id = index.record_id
238
+ if resource?
239
+ entry.record_type = index.record_type
240
+ else
241
+ entry.packed_attributes = index.packed_attributes
242
+ end
243
+
244
+ entry
245
+ end
246
+ end
247
+
248
+ # Custom PDB formats may wish to overide this to support custom appinfo
249
+ # blocks.
250
+ def unpack_appinfo_block(data)
251
+ @appinfo_block = data
252
+ end
253
+
254
+ # Custom PDB formats may wish to overide this to support custom sort
255
+ # blocks.
256
+ def unpack_sort_block(data)
257
+ @sort_block = data
258
+ end
259
+
260
+ public
261
+ # Writes to the given path
262
+ def write_file(path)
263
+ open(path, "w") do |io|
264
+ write io
265
+ end
266
+ end
267
+
268
+ # Writes PDB to an IO object
269
+ def write(io)
270
+ io.binmode if io.respond_to? :binmode
271
+
272
+ # Track the current offset for each section
273
+ offset_position = HEADER_LENGTH + 2 #(2: Index Header length)
274
+
275
+ index_length = RECORD_INDEX_HEADER_LEN +
276
+ @data.length * (resource? ? INDEX_RESOURCE_LENGTH : INDEX_RECORD_LENGTH )
277
+
278
+ offset_position += index_length # Advance for the index
279
+
280
+ packed_entries = @data.map{|e| pack_entry(e)}
281
+
282
+ packed_app_info = pack_app_info_block()
283
+ packed_sort = pack_sort_block()
284
+
285
+ # Calculate AppInfo block offset
286
+ app_info_offset = 0
287
+ if packed_app_info and !packed_app_info.empty?
288
+ app_info_offset = offset_position
289
+ offset_position += packed_app_info.length # Advance for the app_info_block
290
+ end
291
+
292
+ # Calculate sort block offset
293
+ sort_offset = 0
294
+ if packed_sort and !packed_sort.empty?
295
+ sort_offset = offset_position
296
+ offset_position += packed_sort.length # Advance for the sort_block
297
+ end
298
+
299
+ packed_header = pack_header(app_info_offset, sort_offset)
300
+
301
+ index_header = [0, @data.length ].pack "N n"
302
+
303
+ packed_index = @data.zip(packed_entries).map do |entry, packed|
304
+ index = nil
305
+ if resource?
306
+ index = [entry.record_type, entry.record_id, offset_position].pack "a4 n N"
307
+ else
308
+ index = [
309
+ offset_position, entry.packed_attributes,
310
+ (entry.record_id >> 16) & 0xff,
311
+ (entry.record_id >> 8) & 0xff,
312
+ entry.record_id & 0xff
313
+ ].pack "N C C3"
314
+ end
315
+ offset_position += packed.length
316
+ index
317
+ end
318
+
319
+ # Write to IO stream
320
+ io << packed_header
321
+ io << index_header
322
+ io << packed_index.join
323
+ io << "\0\0" # 2 null byte separator
324
+ io << @app_info_block unless app_info_offset == 0
325
+ io << @sort_block unless sort_offset == 0
326
+ io << packed_entries.join
327
+ end
328
+
329
+ protected
330
+ def encode_attributes
331
+ encoded = 0
332
+ @attributes.each do |name,flagged|
333
+ encoded |= ATTRIBUTE_CODES[name] if flagged
334
+ end
335
+
336
+ encoded
337
+ end
338
+
339
+ def pack_header(app_info_offset, sort_offset)
340
+ attributes = encode_attributes
341
+
342
+ header_block = [
343
+ @name, attributes, @version,
344
+ @created_at.to_palm_seconds, @modified_at.to_palm_seconds, @backed_up_at.to_palm_seconds,
345
+ @modnum, app_info_offset, sort_offset,
346
+ @type, @creator,
347
+ @unique_id_seed
348
+ ].pack "a32 n n N N N N N N a4 a4 N"
349
+ header_block
350
+ end
351
+
352
+ # Custom PDB formats must overide this to support their record format.
353
+ def pack_entry(entry)
354
+ entry.data
355
+ end
356
+
357
+ # Custom PDB formats may wish to overide this to support custom sort
358
+ # blocks.
359
+ def pack_sort_block
360
+ @sort_block
361
+ end
362
+
363
+ # Custom PDB formats may wish to overide this to support custom appinfo
364
+ # blocks.
365
+ def pack_app_info_block
366
+ @appinfo_block
367
+ end
368
+ end
369
+
370
+ end