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