perobs 4.0.0 → 4.4.0

Sign up to get free protection for your applications and to get access to all the features.
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)