palm 0.0.1

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