palm 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.txt +1 -0
- data/History.txt +0 -0
- data/Manifest.txt +20 -0
- data/README.txt +41 -0
- data/Rakefile +50 -0
- data/lib/palm.rb +1 -0
- data/lib/palm/palm_record.rb +62 -0
- data/lib/palm/palm_support.rb +12 -0
- data/lib/palm/pdb.rb +370 -0
- data/lib/palm/raw_record.rb +21 -0
- data/lib/palm/version.rb +9 -0
- data/lib/palm/waba_db.rb +44 -0
- data/lib/palm/waba_io.rb +63 -0
- data/lib/palm/waba_record.rb +63 -0
- data/setup.rb +1585 -0
- data/test/HovData.pdb +0 -0
- data/test/pdb_test.rb +97 -0
- data/test/test_helper.rb +7 -0
- data/test/waba_db_test.rb +77 -0
- data/test/waba_records_test.rb +75 -0
- metadata +74 -0
data/CHANGELOG.txt
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.1 - Initial Release
|
data/History.txt
ADDED
File without changes
|
data/Manifest.txt
ADDED
@@ -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
|
data/README.txt
ADDED
@@ -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.
|
data/Rakefile
ADDED
@@ -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
|
+
|
data/lib/palm.rb
ADDED
@@ -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
|
data/lib/palm/pdb.rb
ADDED
@@ -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
|