perobs 4.0.0 → 4.4.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.
Files changed (67) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +27 -16
  3. data/lib/perobs/Array.rb +66 -19
  4. data/lib/perobs/BTree.rb +106 -15
  5. data/lib/perobs/BTreeBlob.rb +4 -3
  6. data/lib/perobs/BTreeDB.rb +5 -4
  7. data/lib/perobs/BTreeNode.rb +482 -156
  8. data/lib/perobs/BTreeNodeLink.rb +10 -0
  9. data/lib/perobs/BigArray.rb +285 -0
  10. data/lib/perobs/BigArrayNode.rb +1002 -0
  11. data/lib/perobs/BigHash.rb +246 -0
  12. data/lib/perobs/BigTree.rb +197 -0
  13. data/lib/perobs/BigTreeNode.rb +873 -0
  14. data/lib/perobs/Cache.rb +48 -10
  15. data/lib/perobs/ConsoleProgressMeter.rb +61 -0
  16. data/lib/perobs/DataBase.rb +4 -3
  17. data/lib/perobs/DynamoDB.rb +57 -15
  18. data/lib/perobs/EquiBlobsFile.rb +155 -50
  19. data/lib/perobs/FNV_Hash_1a_64.rb +54 -0
  20. data/lib/perobs/FlatFile.rb +519 -227
  21. data/lib/perobs/FlatFileBlobHeader.rb +113 -54
  22. data/lib/perobs/FlatFileDB.rb +49 -23
  23. data/lib/perobs/FuzzyStringMatcher.rb +175 -0
  24. data/lib/perobs/Hash.rb +127 -33
  25. data/lib/perobs/IDList.rb +144 -0
  26. data/lib/perobs/IDListPage.rb +107 -0
  27. data/lib/perobs/IDListPageFile.rb +180 -0
  28. data/lib/perobs/IDListPageRecord.rb +142 -0
  29. data/lib/perobs/Object.rb +18 -15
  30. data/lib/perobs/ObjectBase.rb +46 -5
  31. data/lib/perobs/PersistentObjectCache.rb +57 -68
  32. data/lib/perobs/PersistentObjectCacheLine.rb +24 -12
  33. data/lib/perobs/ProgressMeter.rb +97 -0
  34. data/lib/perobs/SpaceManager.rb +273 -0
  35. data/lib/perobs/SpaceTree.rb +21 -12
  36. data/lib/perobs/SpaceTreeNode.rb +53 -61
  37. data/lib/perobs/Store.rb +264 -145
  38. data/lib/perobs/version.rb +1 -1
  39. data/lib/perobs.rb +2 -0
  40. data/perobs.gemspec +4 -4
  41. data/test/Array_spec.rb +15 -6
  42. data/test/BTree_spec.rb +6 -2
  43. data/test/BigArray_spec.rb +261 -0
  44. data/test/BigHash_spec.rb +152 -0
  45. data/test/BigTreeNode_spec.rb +153 -0
  46. data/test/BigTree_spec.rb +259 -0
  47. data/test/EquiBlobsFile_spec.rb +105 -1
  48. data/test/FNV_Hash_1a_64_spec.rb +59 -0
  49. data/test/FlatFileDB_spec.rb +198 -14
  50. data/test/FuzzyStringMatcher_spec.rb +261 -0
  51. data/test/Hash_spec.rb +13 -3
  52. data/test/IDList_spec.rb +77 -0
  53. data/test/LegacyDBs/LegacyDB.rb +155 -0
  54. data/test/LegacyDBs/version_3/class_map.json +1 -0
  55. data/test/LegacyDBs/version_3/config.json +1 -0
  56. data/test/LegacyDBs/version_3/database.blobs +0 -0
  57. data/test/LegacyDBs/version_3/database_spaces.blobs +0 -0
  58. data/test/LegacyDBs/version_3/index.blobs +0 -0
  59. data/test/LegacyDBs/version_3/version +1 -0
  60. data/test/LockFile_spec.rb +9 -6
  61. data/test/SpaceManager_spec.rb +176 -0
  62. data/test/SpaceTree_spec.rb +4 -1
  63. data/test/Store_spec.rb +305 -203
  64. data/test/spec_helper.rb +9 -4
  65. metadata +57 -16
  66. data/lib/perobs/BTreeNodeCache.rb +0 -109
  67. data/lib/perobs/TreeDB.rb +0 -277
data/lib/perobs/Cache.rb CHANGED
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # = Cache.rb -- Persistent Ruby Object Store
4
4
  #
5
- # Copyright (c) 2015, 2016 by Chris Schlaeger <chris@taskjuggler.org>
5
+ # Copyright (c) 2015, 2016, 2019 by Chris Schlaeger <chris@taskjuggler.org>
6
6
  #
7
7
  # MIT License
8
8
  #
@@ -66,10 +66,10 @@ module PEROBS
66
66
  def cache_write(obj)
67
67
  # This is just a safety check. It can probably be disabled in the future
68
68
  # to increase performance.
69
- if obj.respond_to?(:is_poxreference?)
70
- # If this condition triggers, we have a bug in the library.
71
- PEROBS.log.fatal "POXReference objects should never be cached"
72
- end
69
+ #if obj.respond_to?(:is_poxreference?)
70
+ # # If this condition triggers, we have a bug in the library.
71
+ # PEROBS.log.fatal "POXReference objects should never be cached"
72
+ #end
73
73
 
74
74
  if @transaction_stack.empty?
75
75
  # We are not in transaction mode.
@@ -93,16 +93,54 @@ module PEROBS
93
93
  end
94
94
  end
95
95
 
96
- # Return the PEROBS::Object with the specified ID or nil if not found.
96
+ # Evict the object with the given ID from the cache.
97
97
  # @param id [Integer] ID of the cached PEROBS::ObjectBase
98
- def object_by_id(id)
98
+ # @return [True/False] True if object was stored in the cache. False
99
+ # otherwise.
100
+ def evict(id)
101
+ unless @transaction_stack.empty?
102
+ PEROBS.log.fatal "You cannot evict entries during a transaction."
103
+ end
104
+
99
105
  idx = id & @mask
100
106
  # The index is just a hash. We still need to check if the object IDs are
101
107
  # actually the same before we can return the object.
102
108
  if (obj = @writes[idx]) && obj._id == id
103
- # The object was in the write cache.
104
- return obj
109
+ # The object is in the write cache.
110
+ @writes[idx] = nil
111
+ return true
105
112
  elsif (obj = @reads[idx]) && obj._id == id
113
+ # The object is in the read cache.
114
+ @reads[idx] = nil
115
+ return true
116
+ end
117
+
118
+ false
119
+ end
120
+
121
+ # Return the PEROBS::Object with the specified ID or nil if not found.
122
+ # @param id [Integer] ID of the cached PEROBS::ObjectBase
123
+ def object_by_id(id)
124
+ idx = id & @mask
125
+
126
+ if @transaction_stack.empty?
127
+ # The index is just a hash. We still need to check if the object IDs are
128
+ # actually the same before we can return the object.
129
+ if (obj = @writes[idx]) && obj._id == id
130
+ # The object was in the write cache.
131
+ return obj
132
+ end
133
+ else
134
+ # During transactions, the read cache is used to provide fast access
135
+ # to modified objects. But it does not store all modified objects
136
+ # since there can be hash collisions. So we also have to check all
137
+ # transaction objects first.
138
+ if (obj = @transaction_objects[id])
139
+ return obj
140
+ end
141
+ end
142
+
143
+ if (obj = @reads[idx]) && obj._id == id
106
144
  # The object was in the read cache.
107
145
  return obj
108
146
  end
@@ -160,7 +198,7 @@ module PEROBS
160
198
  transactions = @transaction_stack.pop
161
199
  # Merge the two lists
162
200
  @transaction_stack.push(@transaction_stack.pop + transactions)
163
- # Ensure that each object is only included once in the list.
201
+ # Ensure that each object ID is only included once in the list.
164
202
  @transaction_stack.last.uniq!
165
203
  end
166
204
  end
@@ -0,0 +1,61 @@
1
+ # encoding: UTF-8
2
+ #
3
+ # = ConsoleProgressMeter.rb -- Persistent Ruby Object Store
4
+ #
5
+ # Copyright (c) 2018 by Chris Schlaeger <chris@taskjuggler.org>
6
+ #
7
+ # MIT License
8
+ #
9
+ # Permission is hereby granted, free of charge, to any person obtaining
10
+ # a copy of this software and associated documentation files (the
11
+ # "Software"), to deal in the Software without restriction, including
12
+ # without limitation the rights to use, copy, modify, merge, publish,
13
+ # distribute, sublicense, and/or sell copies of the Software, and to
14
+ # permit persons to whom the Software is furnished to do so, subject to
15
+ # the following conditions:
16
+ #
17
+ # The above copyright notice and this permission notice shall be
18
+ # included in all copies or substantial portions of the Software.
19
+ #
20
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
22
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
24
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
25
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
+
28
+ require 'perobs/ProgressMeter'
29
+
30
+ module PEROBS
31
+
32
+ class ConsoleProgressMeter < ProgressMeter
33
+
34
+ LINE_LENGTH = 79
35
+
36
+ private
37
+
38
+ def print_bar
39
+ percent = @max_value == 0 ? 100.0 :
40
+ (@current_value.to_f / @max_value) * 100.0
41
+ percent = 0.0 if percent < 0
42
+ percent = 100.0 if percent > 100.0
43
+
44
+ meter = "<#{percent.to_i}%>"
45
+
46
+ bar_length = LINE_LENGTH - @name.chars.length - 3 - meter.chars.length
47
+ left_bar = '*' * (bar_length * percent / 100.0)
48
+ right_bar = ' ' * (bar_length - left_bar.chars.length)
49
+
50
+ print "\r#{@name} [#{left_bar}#{meter}#{right_bar}]"
51
+ end
52
+
53
+ def print_time
54
+ s = "\r#{@name} [#{secsToHMS(@end_time - @start_time)}]"
55
+ puts s + (' ' * (LINE_LENGTH - s.chars.length + 1))
56
+ end
57
+
58
+ end
59
+
60
+ end
61
+
@@ -2,7 +2,7 @@
2
2
  #
3
3
  # = DataBase.rb -- Persistent Ruby Object Store
4
4
  #
5
- # Copyright (c) 2015 by Chris Schlaeger <chris@taskjuggler.org>
5
+ # Copyright (c) 2015, 2018, 2019 by Chris Schlaeger <chris@taskjuggler.org>
6
6
  #
7
7
  # MIT License
8
8
  #
@@ -42,8 +42,9 @@ module PEROBS
42
42
 
43
43
  # Create a new DataBase object. This method must be overwritten by the
44
44
  # deriving classes and then called via their constructor.
45
- def initialize(serializer = :json)
46
- @serializer = serializer
45
+ def initialize(options)
46
+ @serializer = options[:serializer] || :json
47
+ @progressmeter = options[:progressmeter] || ProgressMeter.new
47
48
  @config = {}
48
49
  end
49
50
 
@@ -2,7 +2,8 @@
2
2
  #
3
3
  # = DynamoDB.rb -- Persistent Ruby Object Store
4
4
  #
5
- # Copyright (c) 2015, 2016 by Chris Schlaeger <chris@taskjuggler.org>
5
+ # Copyright (c) 2015, 2016, 2017, 2018
6
+ # by Chris Schlaeger <chris@taskjuggler.org>
6
7
  #
7
8
  # MIT License
8
9
  #
@@ -25,7 +26,7 @@
25
26
  # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
26
27
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
28
 
28
- require 'aws-sdk'
29
+ require 'aws-sdk-dynamodb'
29
30
 
30
31
  require 'perobs/DataBase'
31
32
  require 'perobs/BTreeBlob'
@@ -35,6 +36,10 @@ module PEROBS
35
36
  # This class implements an Amazon DynamoDB storage engine for PEROBS.
36
37
  class DynamoDB < DataBase
37
38
 
39
+ INTERNAL_ITEMS = %w( config item_counter )
40
+
41
+ attr_reader :item_counter
42
+
38
43
  # Create a new DynamoDB object.
39
44
  # @param db_name [String] name of the DB directory
40
45
  # @param options [Hash] options to customize the behavior. Currently only
@@ -50,7 +55,7 @@ module PEROBS
50
55
  options[:serializer] = :yaml
51
56
  end
52
57
 
53
- super(options[:serializer] || :json)
58
+ super(options)
54
59
 
55
60
  if options.include?(:aws_id) && options.include?(:aws_key)
56
61
  Aws.config[:credentials] = Aws::Credentials.new(options[:aws_id],
@@ -62,12 +67,25 @@ module PEROBS
62
67
 
63
68
  @dynamodb = Aws::DynamoDB::Client.new
64
69
  @table_name = db_name
65
- ensure_table_exists(@table_name)
70
+ @config = nil
71
+ # The number of items currently stored in the DB.
72
+ @item_counter = nil
73
+ if create_table(@table_name)
74
+ @config = { 'serializer' => @serializer }
75
+ put_hash('config', @config)
76
+ @item_counter = 0
77
+ dynamo_put_item('item_counter', @item_counter.to_s)
78
+ else
79
+ @config = get_hash('config')
80
+ if @config['serializer'] != @serializer
81
+ raise ArgumentError, "DynamoDB #{@table_name} was created with " +
82
+ "serializer #{@config['serializer']} but was now opened with " +
83
+ "serializer #{@serializer}."
84
+ end
85
+ @item_counter = dynamo_get_item('item_counter').to_i
86
+ end
66
87
 
67
88
  # Read the existing DB config.
68
- @config = get_hash('config')
69
- check_option('serializer')
70
- put_hash('config', @config)
71
89
  end
72
90
 
73
91
  # Delete the entire database. The database is no longer usable after this
@@ -112,6 +130,13 @@ module PEROBS
112
130
  # Store the given object into the cluster files.
113
131
  # @param obj [Hash] Object as defined by PEROBS::ObjectBase
114
132
  def put_object(obj, id)
133
+ id_str = id.to_s
134
+ unless dynamo_get_item(id_str)
135
+ # The is no object with this ID yet. Increase the item counter.
136
+ @item_counter += 1
137
+ dynamo_put_item('item_counter', @item_counter.to_s)
138
+ end
139
+
115
140
  dynamo_put_item(id.to_s, serialize(obj))
116
141
  end
117
142
 
@@ -128,23 +153,23 @@ module PEROBS
128
153
  each_item do |id|
129
154
  dynamo_mark_item(id, false)
130
155
  end
131
- # Mark the 'config' item so it will not get deleted.
132
- dynamo_mark_item('config')
133
156
  end
134
157
 
135
158
  # Permanently delete all objects that have not been marked. Those are
136
159
  # orphaned and are no longer referenced by any actively used object.
137
- # @return [Array] List of object IDs of the deleted objects.
160
+ # @return [Integer] Count of the deleted objects.
138
161
  def delete_unmarked_objects
139
- deleted_ids = []
162
+ deleted_objects_count = 0
140
163
  each_item do |id|
141
164
  unless dynamo_is_marked?(id)
142
165
  dynamo_delete_item(id)
143
- deleted_ids << id
166
+ deleted_objects_count += 1
167
+ @item_counter -= 1
144
168
  end
145
169
  end
170
+ dynamo_put_item('item_counter', @item_counter.to_s)
146
171
 
147
- deleted_ids
172
+ deleted_objects_count
148
173
  end
149
174
 
150
175
  # Mark an object.
@@ -163,7 +188,17 @@ module PEROBS
163
188
  # @param repair [TrueClass/FalseClass] True if found errors should be
164
189
  # repaired.
165
190
  def check_db(repair = false)
166
- # TODO: See if we can add checks here
191
+ unless (item_counter = dynamo_get_item('item_counter')) &&
192
+ item_counter == @item_counter
193
+ PEROBS.log.error "@item_counter variable (#{@item_counter}) and " +
194
+ "item_counter table entry (#{item_counter}) don't match"
195
+ end
196
+ item_counter = 0
197
+ each_item { item_counter += 1 }
198
+ unless item_counter == @item_counter
199
+ PEROBS.log.error "Table contains #{item_counter} items but " +
200
+ "@item_counter is #{@item_counter}"
201
+ end
167
202
  end
168
203
 
169
204
  # Check if the stored object is syntactically correct.
@@ -185,9 +220,11 @@ module PEROBS
185
220
 
186
221
  private
187
222
 
188
- def ensure_table_exists(table_name)
223
+ def create_table(table_name)
189
224
  begin
190
225
  @dynamodb.describe_table(:table_name => table_name)
226
+ # The table exists already. No need to create it.
227
+ return false
191
228
  rescue Aws::DynamoDB::Errors::ResourceNotFoundException
192
229
  @dynamodb.create_table(
193
230
  :table_name => table_name,
@@ -210,6 +247,8 @@ module PEROBS
210
247
  )
211
248
 
212
249
  @dynamodb.wait_until(:table_exists, table_name: table_name)
250
+ # The table was successfully created.
251
+ return true
213
252
  end
214
253
  end
215
254
 
@@ -251,6 +290,9 @@ module PEROBS
251
290
  break if resp.count <= 0
252
291
 
253
292
  resp.items.each do |item|
293
+ # Skip all internal items
294
+ next if INTERNAL_ITEMS.include?(item['Id'])
295
+
254
296
  yield(item['Id'])
255
297
  end
256
298
 
@@ -2,7 +2,8 @@
2
2
  #
3
3
  # = EquiBlobsFile.rb -- Persistent Ruby Object Store
4
4
  #
5
- # Copyright (c) 2016, 2017 by Chris Schlaeger <chris@taskjuggler.org>
5
+ # Copyright (c) 2016, 2017, 2018, 2019
6
+ # by Chris Schlaeger <chris@taskjuggler.org>
6
7
  #
7
8
  # MIT License
8
9
  #
@@ -26,6 +27,7 @@
26
27
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
27
28
 
28
29
  require 'perobs/Log'
30
+ require 'perobs/ProgressMeter'
29
31
 
30
32
  module PEROBS
31
33
 
@@ -37,6 +39,11 @@ module PEROBS
37
39
  # is used to represent an undefined address or nil. The file has a 4 * 8
38
40
  # bytes long header that stores the total entry count, the total space
39
41
  # count, the offset of the first entry and the offset of the first space.
42
+ # The header is followed by a custom entry section. Each entry is also 8
43
+ # bytes long. After the custom entry section the data blobs start. Each data
44
+ # blob starts with a mark byte that indicates if the blob is valid data (2),
45
+ # a free space (0) or reseved space (1). Then it is followed by @entry_bytes
46
+ # number of bytes for the data blob.
40
47
  class EquiBlobsFile
41
48
 
42
49
  TOTAL_ENTRIES_OFFSET = 0
@@ -50,15 +57,21 @@ module PEROBS
50
57
  # Create a new stack file in the given directory with the given file name.
51
58
  # @param dir [String] Directory
52
59
  # @param name [String] File name
60
+ # @param progressmeter [ProgressMeter] Reference to a progress meter
61
+ # object
53
62
  # @param entry_bytes [Integer] Number of bytes each entry must have
54
63
  # @param first_entry_default [Integer] Default address of the first blob
55
- def initialize(dir, name, entry_bytes, first_entry_default = 0)
64
+ def initialize(dir, name, progressmeter, entry_bytes,
65
+ first_entry_default = 0)
66
+ @name = name
56
67
  @file_name = File.join(dir, name + '.blobs')
68
+ @progressmeter = progressmeter
57
69
  if entry_bytes < 8
58
70
  PEROBS.log.fatal "EquiBlobsFile entry size must be at least 8"
59
71
  end
60
72
  @entry_bytes = entry_bytes
61
73
  @first_entry_default = first_entry_default
74
+ clear_custom_data
62
75
  reset_counters
63
76
 
64
77
  # The File handle.
@@ -102,6 +115,57 @@ module PEROBS
102
115
  end
103
116
  end
104
117
 
118
+ # In addition to the standard offsets for the first entry and the first
119
+ # space any number of additional data fields can be registered. This must be
120
+ # done right after the object is instanciated and before the open() method
121
+ # is called. Each field represents a 64 bit unsigned integer.
122
+ # @param name [String] The label for this offset
123
+ # @param default_value [Integer] The default value for the offset
124
+ def register_custom_data(name, default_value = 0)
125
+ if @custom_data_labels.include?(name)
126
+ PEROBS.log.fatal "Custom data field #{name} has already been registered"
127
+ end
128
+
129
+ @custom_data_labels << name
130
+ @custom_data_values << default_value
131
+ @custom_data_defaults << default_value
132
+ end
133
+
134
+ # Reset (delete) all custom data labels that have been registered.
135
+ def clear_custom_data
136
+ unless @f.nil?
137
+ PEROBS.log.fatal "clear_custom_data should only be called when " +
138
+ "the file is not opened"
139
+ end
140
+
141
+ @custom_data_labels = []
142
+ @custom_data_values = []
143
+ @custom_data_defaults = []
144
+ end
145
+
146
+ # Set the registered custom data field to the given value.
147
+ # @param name [String] Label of the offset
148
+ # @param value [Integer] Value
149
+ def set_custom_data(name, value)
150
+ unless @custom_data_labels.include?(name)
151
+ PEROBS.log.fatal "Unknown custom data field #{name}"
152
+ end
153
+
154
+ @custom_data_values[@custom_data_labels.index(name)] = value
155
+ write_header if @f
156
+ end
157
+
158
+ # Get the registered custom data field value.
159
+ # @param name [String] Label of the offset
160
+ # @return [Integer] Value of the custom data field
161
+ def get_custom_data(name)
162
+ unless @custom_data_labels.include?(name)
163
+ PEROBS.log.fatal "Unknown custom data field #{name}"
164
+ end
165
+
166
+ @custom_data_values[@custom_data_labels.index(name)]
167
+ end
168
+
105
169
  # Erase the backing store. This method should only be called when the file
106
170
  # is not currently open.
107
171
  def erase
@@ -285,7 +349,7 @@ module PEROBS
285
349
 
286
350
  @first_space = offset
287
351
  @total_spaces += 1
288
- @total_entries -= 1
352
+ @total_entries -= 1 unless marker == 1
289
353
  write_header
290
354
 
291
355
  if offset == @f.size - 1 - @entry_bytes
@@ -298,12 +362,16 @@ module PEROBS
298
362
  # Check the file for logical errors.
299
363
  # @return [Boolean] true of file has no errors, false otherwise.
300
364
  def check
365
+ sync
366
+
301
367
  return false unless check_spaces
302
368
  return false unless check_entries
303
369
 
304
- if @f.size != HEADER_SIZE + (@total_entries + @total_spaces) *
305
- (1 + @entry_bytes)
306
- PEROBS.log.error "Size mismatch in EquiBlobsFile #{@file_name}"
370
+ expected_size = address_to_offset(@total_entries + @total_spaces + 1)
371
+ actual_size = @f.size
372
+ if actual_size != expected_size
373
+ PEROBS.log.error "Size mismatch in EquiBlobsFile #{@file_name}. " +
374
+ "Expected #{expected_size} bytes but found #{actual_size} bytes."
307
375
  return false
308
376
  end
309
377
 
@@ -326,6 +394,9 @@ module PEROBS
326
394
  @first_entry = @first_entry_default
327
395
  # The file offset of the first empty entry.
328
396
  @first_space = 0
397
+
398
+ # Copy default custom values
399
+ @custom_data_values = @custom_data_defaults.dup
329
400
  end
330
401
 
331
402
  def read_header
@@ -333,6 +404,12 @@ module PEROBS
333
404
  @f.seek(0)
334
405
  @total_entries, @total_spaces, @first_entry, @first_space =
335
406
  @f.read(HEADER_SIZE).unpack('QQQQ')
407
+ custom_labels_count = @custom_data_labels.length
408
+ if custom_labels_count > 0
409
+ @custom_data_values =
410
+ @f.read(custom_labels_count * 8).unpack("Q#{custom_labels_count}")
411
+ end
412
+
336
413
  rescue IOError => e
337
414
  PEROBS.log.fatal "Cannot read EquiBlobsFile header: #{e.message}"
338
415
  end
@@ -343,7 +420,13 @@ module PEROBS
343
420
  begin
344
421
  @f.seek(0)
345
422
  @f.write(header_ary.pack('QQQQ'))
423
+ unless @custom_data_values.empty?
424
+ @f.write(@custom_data_values.
425
+ pack("Q#{@custom_data_values.length}"))
426
+ end
346
427
  @f.flush
428
+ rescue IOError => e
429
+ PEROBS.log.fatal "Cannot write EquiBlobsFile header: " + e.message
347
430
  end
348
431
  end
349
432
 
@@ -367,25 +450,31 @@ module PEROBS
367
450
  return false
368
451
  end
369
452
 
453
+ return true if next_offset == 0
454
+
370
455
  total_spaces = 0
371
- begin
372
- while next_offset != 0
373
- # Check that the marker byte is 0
374
- @f.seek(next_offset)
375
- if (marker = read_char) != 0
376
- PEROBS.log.error "Marker byte at address " +
377
- "#{offset_to_address(next_offset)} is #{marker} instead of 0."
378
- return false
379
- end
380
- # Read offset of next empty space
381
- next_offset = read_unsigned_int
456
+ @progressmeter.start("Checking #{@name} spaces list",
457
+ @total_spaces) do |pm|
458
+ begin
459
+ while next_offset != 0
460
+ # Check that the marker byte is 0
461
+ @f.seek(next_offset)
462
+ if (marker = read_char) != 0
463
+ PEROBS.log.error "Marker byte at address " +
464
+ "#{offset_to_address(next_offset)} is #{marker} instead of 0."
465
+ return false
466
+ end
467
+ # Read offset of next empty space
468
+ next_offset = read_unsigned_int
382
469
 
383
- total_spaces += 1
470
+ total_spaces += 1
471
+ pm.update(total_spaces)
472
+ end
473
+ rescue IOError => e
474
+ PEROBS.log.error "Cannot check space list of EquiBlobsFile " +
475
+ "#{@file_name}: #{e.message}"
476
+ return false
384
477
  end
385
- rescue IOError => e
386
- PEROBS.log.error "Cannot check space list of EquiBlobsFile " +
387
- "#{@file_name}: #{e.message}"
388
- return false
389
478
  end
390
479
 
391
480
  unless total_spaces == @total_spaces
@@ -414,35 +503,48 @@ module PEROBS
414
503
  return false
415
504
  end
416
505
 
417
- next_offset = HEADER_SIZE
506
+ next_offset = address_to_offset(1)
418
507
  total_entries = 0
419
508
  total_spaces = 0
420
- begin
421
- @f.seek(next_offset)
422
- while !@f.eof
423
- marker, bytes = @f.read(1 + @entry_bytes).
424
- unpack("C#{1 + @entry_bytes}")
425
- case marker
426
- when 0
427
- total_spaces += 1
428
- when 1
429
- PEROBS.log.error "Entry at address " +
430
- "#{offset_to_address(next_offset)} in EquiBlobsFile " +
431
- "#{@file_name} has reserved marker"
432
- return false
433
- when 2
434
- total_entries += 1
435
- else
436
- PEROBS.log.error "Entry at address " +
437
- "#{offset_to_address(next_offset)} in EquiBlobsFile " +
438
- "#{@file_name} has illegal marker #{marker}"
439
- return false
509
+ last_entry_is_space = false
510
+ @progressmeter.start("Checking #{@name} entries",
511
+ @total_spaces + @total_entries) do |pm|
512
+ begin
513
+ @f.seek(next_offset)
514
+ while !@f.eof
515
+ marker, bytes = @f.read(1 + @entry_bytes).
516
+ unpack("C#{1 + @entry_bytes}")
517
+ case marker
518
+ when 0
519
+ total_spaces += 1
520
+ last_entry_is_space = true
521
+ when 1
522
+ PEROBS.log.error "Entry at address " +
523
+ "#{offset_to_address(next_offset)} in EquiBlobsFile " +
524
+ "#{@file_name} has reserved marker"
525
+ return false
526
+ when 2
527
+ total_entries += 1
528
+ last_entry_is_space = false
529
+ else
530
+ PEROBS.log.error "Entry at address " +
531
+ "#{offset_to_address(next_offset)} in EquiBlobsFile " +
532
+ "#{@file_name} has illegal marker #{marker}"
533
+ return false
534
+ end
535
+ next_offset += 1 + @entry_bytes
440
536
  end
441
- next_offset += 1 + @entry_bytes
537
+
538
+ pm.update(total_spaces + total_entries)
539
+ rescue
540
+ PEROBS.log.error "Cannot check entries of EquiBlobsFile " +
541
+ "#{@file_name}: #{e.message}"
542
+ return false
442
543
  end
443
- rescue
444
- PEROBS.log.error "Cannot check entries of EquiBlobsFile " +
445
- "#{@file_name}: #{e.message}"
544
+ end
545
+
546
+ if last_entry_is_space
547
+ PEROBS.log.error "EquiBlobsFile #{@file_name} is not properly trimmed"
446
548
  return false
447
549
  end
448
550
 
@@ -464,7 +566,7 @@ module PEROBS
464
566
 
465
567
  def trim_file
466
568
  offset = @f.size - 1 - @entry_bytes
467
- while offset >= HEADER_SIZE
569
+ while offset >= address_to_offset(1)
468
570
  @f.seek(offset)
469
571
  begin
470
572
  if (marker = read_char) == 0
@@ -548,12 +650,15 @@ module PEROBS
548
650
 
549
651
  # Translate a blob address to the actual offset in the file.
550
652
  def address_to_offset(address)
551
- HEADER_SIZE + (address - 1) * (1 + @entry_bytes)
653
+ # Since address 0 is illegal, we can use address - 1 as index here.
654
+ HEADER_SIZE + @custom_data_labels.length * 8 +
655
+ (address - 1) * (1 + @entry_bytes)
552
656
  end
553
657
 
554
658
  # Translate the file offset to the address of a blob.
555
659
  def offset_to_address(offset)
556
- (offset - HEADER_SIZE) / (1 + @entry_bytes) + 1
660
+ (offset - HEADER_SIZE - @custom_data_labels.length * 8) /
661
+ (1 + @entry_bytes) + 1
557
662
  end
558
663
 
559
664
  def write_char(c)