herringbone 0.5.0 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/MANUAL.md +773 -0
- data/README.md +68 -437
- data/bin/herringbone +86 -7
- data/lib/herringbone/bloom_filter.rb +0 -55
- data/lib/herringbone/compression.rb +27 -4
- data/lib/herringbone/encryption.rb +716 -0
- data/lib/herringbone/encryption_configuration.rb +304 -0
- data/lib/herringbone/format.rb +55 -3
- data/lib/herringbone/inferring_writer.rb +17 -2
- data/lib/herringbone/inspector.rb +158 -28
- data/lib/herringbone/key.rb +107 -0
- data/lib/herringbone/reader/bloom_filters.rb +91 -0
- data/lib/herringbone/reader/column_chunk_reader.rb +55 -1
- data/lib/herringbone/reader.rb +100 -15
- data/lib/herringbone/redaction/rewriter.rb +59 -7
- data/lib/herringbone/redaction.rb +20 -5
- data/lib/herringbone/schema.rb +2 -0
- data/lib/herringbone/simple_writer.rb +28 -0
- data/lib/herringbone/thrift.rb +118 -59
- data/lib/herringbone/version.rb +1 -1
- data/lib/herringbone/writer.rb +102 -24
- data/lib/herringbone.rb +69 -25
- metadata +7 -2
data/lib/herringbone/thrift.rb
CHANGED
|
@@ -45,6 +45,14 @@ module Herringbone
|
|
|
45
45
|
double: T_DOUBLE, binary: T_BINARY, string: T_BINARY
|
|
46
46
|
}.freeze
|
|
47
47
|
|
|
48
|
+
# Values each declared integer type can hold, whatever width it came in on the wire
|
|
49
|
+
INT_RANGES = {
|
|
50
|
+
i16: -2**15...2**15, i32: -2**31...2**31, i64: -2**63...2**63
|
|
51
|
+
}.freeze
|
|
52
|
+
|
|
53
|
+
# Deepest nesting of structs, lists, sets and maps read or skipped, as in Apache Thrift
|
|
54
|
+
MAX_DEPTH = 64
|
|
55
|
+
|
|
48
56
|
# Wire type written for a declared field type
|
|
49
57
|
#
|
|
50
58
|
# @param type [Symbol, Array, Class] a scalar type from WIRE_TYPES, a +[:list, elem]+ Array,
|
|
@@ -89,6 +97,7 @@ module Herringbone
|
|
|
89
97
|
def initialize(buf, pos = 0)
|
|
90
98
|
@buf = buf
|
|
91
99
|
@pos = pos
|
|
100
|
+
@depth = 0
|
|
92
101
|
end
|
|
93
102
|
|
|
94
103
|
# Reads one raw byte
|
|
@@ -104,17 +113,21 @@ module Herringbone
|
|
|
104
113
|
|
|
105
114
|
# Reads an unsigned LEB128 varint
|
|
106
115
|
#
|
|
107
|
-
# @return [Integer] the decoded non-negative value
|
|
108
|
-
# @raise [Error] when the varint runs past the buffer
|
|
116
|
+
# @return [Integer] the decoded non-negative value, below 2**64
|
|
117
|
+
# @raise [Error] when the varint runs past the buffer, is longer than 10 bytes or does not
|
|
118
|
+
# fit in 64 bits
|
|
109
119
|
def read_varint
|
|
110
120
|
result = 0
|
|
111
121
|
shift = 0
|
|
112
122
|
while true
|
|
113
123
|
b = read_byte
|
|
114
124
|
result |= (b & 0x7F) << shift
|
|
115
|
-
|
|
125
|
+
if b < 0x80
|
|
126
|
+
raise Error, "Varint exceeds 64 bits" if result >= 2**64
|
|
127
|
+
return result
|
|
128
|
+
end
|
|
116
129
|
shift += 7
|
|
117
|
-
raise Error, "Varint too long" if shift >
|
|
130
|
+
raise Error, "Varint too long" if shift > 63
|
|
118
131
|
end
|
|
119
132
|
end
|
|
120
133
|
|
|
@@ -141,8 +154,10 @@ module Herringbone
|
|
|
141
154
|
|
|
142
155
|
# Reads an 8-byte little-endian double
|
|
143
156
|
#
|
|
144
|
-
# @return [Float
|
|
157
|
+
# @return [Float] the value
|
|
158
|
+
# @raise [Error] when fewer than 8 bytes are left
|
|
145
159
|
def read_double
|
|
160
|
+
raise Error, "Unexpected end of Thrift data at #{@pos}" if @pos + 8 > @buf.bytesize
|
|
146
161
|
v = @buf.byteslice(@pos, 8).unpack1("E")
|
|
147
162
|
@pos += 8
|
|
148
163
|
v
|
|
@@ -155,26 +170,28 @@ module Herringbone
|
|
|
155
170
|
#
|
|
156
171
|
# @param klass [Class] a Thrift::Struct subclass
|
|
157
172
|
# @return [Struct] an instance of +klass+ with the fields that were present set
|
|
158
|
-
# @raise [Error] on truncated or malformed data
|
|
173
|
+
# @raise [Error] on truncated or malformed data, or nesting deeper than MAX_DEPTH
|
|
159
174
|
def read_struct(klass)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
+
nested do
|
|
176
|
+
obj = klass.new
|
|
177
|
+
fields = klass.fields_by_id
|
|
178
|
+
last_id = 0
|
|
179
|
+
while true
|
|
180
|
+
header = read_byte
|
|
181
|
+
wire = header & 0x0F
|
|
182
|
+
break if wire == T_STOP
|
|
183
|
+
delta = header >> 4
|
|
184
|
+
fid = delta.zero? ? read_zigzag : last_id + delta
|
|
185
|
+
last_id = fid
|
|
186
|
+
field = fields[fid]
|
|
187
|
+
if field && Thrift.compatible?(wire, field.type)
|
|
188
|
+
obj.instance_variable_set(field.ivar, read_value(wire, field.type))
|
|
189
|
+
else
|
|
190
|
+
skip(wire)
|
|
191
|
+
end
|
|
175
192
|
end
|
|
193
|
+
obj
|
|
176
194
|
end
|
|
177
|
-
obj
|
|
178
195
|
end
|
|
179
196
|
|
|
180
197
|
# Reads one value of wire type +wire+ as declared +type+
|
|
@@ -184,7 +201,8 @@ module Herringbone
|
|
|
184
201
|
# an Array or Struct subclass gives the element type or struct to read
|
|
185
202
|
# @return [Object] true/false, an Integer (bytes are signed), a Float, a String, an Array
|
|
186
203
|
# (or nil, see #read_list) or a Struct
|
|
187
|
-
# @raise [Error] on an unsupported wire type or
|
|
204
|
+
# @raise [Error] on an unsupported wire type, truncated data or an integer out of the range
|
|
205
|
+
# of +type+
|
|
188
206
|
def read_value(wire, type)
|
|
189
207
|
case wire
|
|
190
208
|
when T_TRUE then true
|
|
@@ -192,7 +210,10 @@ module Herringbone
|
|
|
192
210
|
when T_BYTE
|
|
193
211
|
b = read_byte
|
|
194
212
|
(b >= 0x80) ? b - 0x100 : b
|
|
195
|
-
when T_I16, T_I32, T_I64
|
|
213
|
+
when T_I16, T_I32, T_I64
|
|
214
|
+
n = read_zigzag
|
|
215
|
+
raise Error, "Integer #{n} out of range for #{type}" unless INT_RANGES.fetch(type).cover?(n)
|
|
216
|
+
n
|
|
196
217
|
when T_DOUBLE then read_double
|
|
197
218
|
when T_BINARY
|
|
198
219
|
s = read_binary
|
|
@@ -210,24 +231,26 @@ module Herringbone
|
|
|
210
231
|
# @param type [Array] declared list type, +[:list, elem_type]+
|
|
211
232
|
# @return [Array, nil] the elements, or nil (with the list skipped) when the element wire
|
|
212
233
|
# type does not match +elem_type+
|
|
213
|
-
# @raise [Error] on truncated or malformed data
|
|
234
|
+
# @raise [Error] on truncated or malformed data, or nesting deeper than MAX_DEPTH
|
|
214
235
|
def read_list(type)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
size.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
236
|
+
nested do
|
|
237
|
+
header = read_byte
|
|
238
|
+
size = header >> 4
|
|
239
|
+
size = read_size if size == 15
|
|
240
|
+
elem_wire = header & 0x0F
|
|
241
|
+
elem_type = type[1]
|
|
242
|
+
bool_elems = elem_wire == T_TRUE || elem_wire == T_FALSE
|
|
243
|
+
if size.positive? && !(bool_elems && elem_type == :bool) && !Thrift.compatible?(elem_wire, elem_type)
|
|
244
|
+
size.times { bool_elems ? read_byte : skip(elem_wire) }
|
|
245
|
+
return nil
|
|
246
|
+
end
|
|
247
|
+
Array.new(size) do
|
|
248
|
+
if elem_wire == T_TRUE || elem_wire == T_FALSE
|
|
249
|
+
# Booleans inside lists are encoded as full bytes
|
|
250
|
+
read_byte == T_TRUE
|
|
251
|
+
else
|
|
252
|
+
read_value(elem_wire, elem_type)
|
|
253
|
+
end
|
|
231
254
|
end
|
|
232
255
|
end
|
|
233
256
|
end
|
|
@@ -236,40 +259,76 @@ module Herringbone
|
|
|
236
259
|
#
|
|
237
260
|
# @param wire [Integer] wire type of the value to skip
|
|
238
261
|
# @return [void]
|
|
239
|
-
# @raise [Error] on an unknown wire type (including T_STOP) or
|
|
262
|
+
# @raise [Error] on an unknown wire type (including T_STOP), truncated data or nesting deeper
|
|
263
|
+
# than MAX_DEPTH
|
|
240
264
|
def skip(wire)
|
|
241
265
|
case wire
|
|
242
266
|
when T_TRUE, T_FALSE then nil
|
|
243
267
|
when T_BYTE then read_byte
|
|
244
268
|
when T_I16, T_I32, T_I64 then read_varint
|
|
245
|
-
when T_DOUBLE then
|
|
269
|
+
when T_DOUBLE then read_double
|
|
246
270
|
when T_BINARY then read_binary
|
|
247
271
|
when T_LIST, T_SET
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
272
|
+
nested do
|
|
273
|
+
header = read_byte
|
|
274
|
+
size = header >> 4
|
|
275
|
+
size = read_size if size == 15
|
|
276
|
+
elem = header & 0x0F
|
|
277
|
+
size.times { (elem == T_TRUE || elem == T_FALSE) ? read_byte : skip(elem) }
|
|
278
|
+
end
|
|
253
279
|
when T_MAP
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
280
|
+
nested do
|
|
281
|
+
size = read_size
|
|
282
|
+
unless size.zero?
|
|
283
|
+
kv = read_byte
|
|
284
|
+
size.times do
|
|
285
|
+
[kv >> 4, kv & 0x0F].each { |w| (w == T_TRUE || w == T_FALSE) ? read_byte : skip(w) }
|
|
286
|
+
end
|
|
259
287
|
end
|
|
260
288
|
end
|
|
261
289
|
when T_STRUCT
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
290
|
+
nested do
|
|
291
|
+
while true
|
|
292
|
+
header = read_byte
|
|
293
|
+
w = header & 0x0F
|
|
294
|
+
break if w == T_STOP
|
|
295
|
+
read_zigzag if (header >> 4).zero?
|
|
296
|
+
skip(w)
|
|
297
|
+
end
|
|
268
298
|
end
|
|
269
299
|
else
|
|
270
300
|
raise Error, "Cannot skip wire type #{wire}"
|
|
271
301
|
end
|
|
272
302
|
end
|
|
303
|
+
|
|
304
|
+
private
|
|
305
|
+
|
|
306
|
+
# Reads the element count of a list, set or map
|
|
307
|
+
#
|
|
308
|
+
# Every element takes at least one byte, so a count above the bytes left is corrupt. Checking
|
|
309
|
+
# that up front keeps a forged count from preallocating a huge Array or skipping for ever.
|
|
310
|
+
#
|
|
311
|
+
# @return [Integer] the count, at most the number of bytes left in the buffer
|
|
312
|
+
# @raise [Error] when the count exceeds the bytes left
|
|
313
|
+
def read_size
|
|
314
|
+
size = read_varint
|
|
315
|
+
left = @buf.bytesize - @pos
|
|
316
|
+
raise Error, "Collection of #{size} elements exceeds the #{left} bytes left" if size > left
|
|
317
|
+
size
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# Runs the block one nesting level deeper
|
|
321
|
+
#
|
|
322
|
+
# @yieldreturn [Object] the value read inside the nested container
|
|
323
|
+
# @return [Object] the block's value
|
|
324
|
+
# @raise [Error] when the nesting gets deeper than MAX_DEPTH
|
|
325
|
+
def nested
|
|
326
|
+
@depth += 1
|
|
327
|
+
raise Error, "Thrift data nested deeper than #{MAX_DEPTH} levels" if @depth > MAX_DEPTH
|
|
328
|
+
yield
|
|
329
|
+
ensure
|
|
330
|
+
@depth -= 1
|
|
331
|
+
end
|
|
273
332
|
end
|
|
274
333
|
|
|
275
334
|
# Encodes compact protocol data by appending to a binary String
|
data/lib/herringbone/version.rb
CHANGED
data/lib/herringbone/writer.rb
CHANGED
|
@@ -24,6 +24,8 @@ module Herringbone
|
|
|
24
24
|
# Options:
|
|
25
25
|
# compression: :snappy (default), :zstd, :gzip, :lz4 (LZ4_RAW), :lz4_hadoop, :brotli, :none
|
|
26
26
|
# (:zstd and :brotli need the zstd-ruby / brotli gems)
|
|
27
|
+
# compression_level: nil (the codec's default), or a level for :zstd (up to 22), :gzip (0-9)
|
|
28
|
+
# or :brotli (0-11)
|
|
27
29
|
# row_group_bytes: flush a row group once the buffered values take roughly this much memory
|
|
28
30
|
# (default 16MB). This bounds memory use while writing.
|
|
29
31
|
# row_group_rows: also flush after this many rows (default: no row limit)
|
|
@@ -38,6 +40,9 @@ module Herringbone
|
|
|
38
40
|
# an Array of column paths, or { "path" => true | { ndv:, fpp:, max_bytes: } }.
|
|
39
41
|
# Without ndv: the distinct values of each row group are counted. fpp defaults
|
|
40
42
|
# to 0.01 and max_bytes to 1MB. Filters are written after each row group.
|
|
43
|
+
# encryption: Parquet modular encryption: { footer_key: "...", columns: { "ssn" => "..." } }.
|
|
44
|
+
# Without columns: every column is encrypted with the footer key. See
|
|
45
|
+
# #initialize for the settings.
|
|
41
46
|
#
|
|
42
47
|
# Statistics and page indexes (ColumnIndex/OffsetIndex) are always written.
|
|
43
48
|
class Writer
|
|
@@ -83,6 +88,7 @@ module Herringbone
|
|
|
83
88
|
# @param schema [Schema] schema of the rows
|
|
84
89
|
# @param options [Hash{Symbol => Object}] see the class description and #initialize
|
|
85
90
|
# @option options [Symbol] :compression (:snappy) codec, see Herringbone.codecs
|
|
91
|
+
# @option options [Integer, nil] :compression_level (nil) level for :zstd, :gzip or :brotli
|
|
86
92
|
# @option options [Integer] :row_group_bytes (16MB) approximate buffered size that triggers a row group
|
|
87
93
|
# @option options [Integer, nil] :row_group_rows (nil) also flush a row group after this many rows
|
|
88
94
|
# @option options [Integer] :page_bytes (1MB) approximate uncompressed data page size
|
|
@@ -95,6 +101,8 @@ module Herringbone
|
|
|
95
101
|
# @option options [Hash{String => String}] :metadata ({}) key/value metadata for the footer
|
|
96
102
|
# @option options [Boolean, Array<String>, Hash{String => Boolean, Hash}] :bloom_filters (nil)
|
|
97
103
|
# columns to write split block bloom filters for
|
|
104
|
+
# @option options [EncryptionConfiguration, Hash{Symbol => Object}, nil] :encryption (nil) modular
|
|
105
|
+
# encryption settings, see #initialize
|
|
98
106
|
# @yield [writer] the open writer
|
|
99
107
|
# @yieldparam writer [Writer] writer to append rows to
|
|
100
108
|
# @yieldreturn [Object] returned by open
|
|
@@ -118,6 +126,8 @@ module Herringbone
|
|
|
118
126
|
# @param io [IO, #write] destination; switched to binmode when it supports that
|
|
119
127
|
# @param schema [Schema] schema of the rows
|
|
120
128
|
# @param compression [Symbol, Integer] codec name (see Herringbone.codecs) or Format::Codec id
|
|
129
|
+
# @param compression_level [Integer, nil] level for :zstd, :gzip or :brotli (see
|
|
130
|
+
# Compression::LEVELS), nil for the codec's default
|
|
121
131
|
# @param row_group_bytes [Integer] approximate buffered size that triggers a row group
|
|
122
132
|
# @param row_group_rows [Integer, nil] also flush a row group after this many rows
|
|
123
133
|
# @param page_bytes [Integer] approximate uncompressed data page size
|
|
@@ -131,17 +141,35 @@ module Herringbone
|
|
|
131
141
|
# @param metadata [Hash{#to_s => #to_s}] key/value metadata for the footer
|
|
132
142
|
# @param bloom_filters [Boolean, Array<String>, Hash{String => Boolean, Hash}, nil] see the class
|
|
133
143
|
# description
|
|
134
|
-
# @
|
|
144
|
+
# @param encryption [EncryptionConfiguration, Hash{Symbol => Object}, false, nil] encrypts the file
|
|
145
|
+
# (Parquet modular encryption); a Hash holds the keywords of EncryptionConfiguration.new.
|
|
146
|
+
# Keys are 16, 24 or 32-byte Strings; key metadata is stored as given, for readers to find
|
|
147
|
+
# the keys by.
|
|
148
|
+
# @option encryption [String] :footer_key key of the footer, and of columns without their own
|
|
149
|
+
# @option encryption [String, nil] :footer_key_metadata stored for the footer key
|
|
150
|
+
# @option encryption [Hash{String => String, Hash, Symbol}, nil] :columns column path or field
|
|
151
|
+
# name => key, +{key:, key_metadata:}+ or +:footer+. Columns not listed are not encrypted;
|
|
152
|
+
# without this option every column is, with the footer key.
|
|
153
|
+
# @option encryption [Boolean] :plaintext_footer (false) store the footer in the clear (signed),
|
|
154
|
+
# so readers without keys can read the plaintext columns
|
|
155
|
+
# @option encryption [Symbol] :algorithm (:aes_gcm) or :aes_gcm_ctr, which encrypts pages with
|
|
156
|
+
# AES-CTR (faster, but page contents are not authenticated)
|
|
157
|
+
# @option encryption [String, nil] :aad_prefix identity of the file, which readers can check
|
|
158
|
+
# @option encryption [Boolean] :store_aad_prefix (true) false leaves it out of the file, so
|
|
159
|
+
# readers must supply it
|
|
160
|
+
# @raise [ArgumentError] for an invalid IO, schema or option, or a compression level the codec
|
|
161
|
+
# does not take
|
|
135
162
|
# @raise [MissingCodecError] when the codec's optional gem is not loaded
|
|
136
163
|
# @raise [UnsupportedError] when the codec is not supported
|
|
137
|
-
def initialize(io, schema, compression: :snappy, row_group_bytes: 16 * 1024 * 1024, row_group_rows: nil,
|
|
164
|
+
def initialize(io, schema, compression: :snappy, compression_level: nil, row_group_bytes: 16 * 1024 * 1024, row_group_rows: nil,
|
|
138
165
|
page_bytes: 1024 * 1024, page_rows: 20_000, data_page_version: 1, dictionary: true, encodings: {},
|
|
139
|
-
metadata: {}, bloom_filters: nil)
|
|
166
|
+
metadata: {}, bloom_filters: nil, encryption: nil)
|
|
140
167
|
raise ArgumentError, "Expected a Herringbone::Schema, got #{schema.class}" unless schema.is_a?(Schema)
|
|
141
168
|
@schema = schema
|
|
142
169
|
@codec = Compression.codec_id(compression)
|
|
143
170
|
# Fail before creating any file if the codec's library is missing
|
|
144
171
|
Compression.ensure_available!(@codec)
|
|
172
|
+
@compression_level = Compression.check_level!(@codec, compression_level)
|
|
145
173
|
@row_group_bytes = Integer(row_group_bytes)
|
|
146
174
|
@row_group_rows = row_group_rows && Integer(row_group_rows)
|
|
147
175
|
@row_limit = @row_group_rows || ESTIMATE_AFTER_ROWS
|
|
@@ -159,7 +187,8 @@ module Herringbone
|
|
|
159
187
|
@encodings.each { |path, enc| check_encoding!(columns[path], enc) }
|
|
160
188
|
@metadata = metadata
|
|
161
189
|
@bloom_filters = bloom_filter_config(bloom_filters)
|
|
162
|
-
@pending_bloom_filters = [] # [ColumnMetaData, BloomFilter] for the row group being written
|
|
190
|
+
@pending_bloom_filters = [] # [ColumnMetaData, BloomFilter, ModuleCrypto] for the row group being written
|
|
191
|
+
@encryption = encryption ? Encryption::FileEncryptor.new(EncryptionConfiguration.from(encryption), schema) : nil
|
|
163
192
|
@row_groups = []
|
|
164
193
|
@total_rows = 0
|
|
165
194
|
@pos = 0
|
|
@@ -167,7 +196,7 @@ module Herringbone
|
|
|
167
196
|
@aborted = false
|
|
168
197
|
@bytes_per_row = nil
|
|
169
198
|
@io = check_io!(io)
|
|
170
|
-
write_raw(MAGIC)
|
|
199
|
+
write_raw(@encryption ? @encryption.magic : MAGIC)
|
|
171
200
|
reset_buffers
|
|
172
201
|
end
|
|
173
202
|
|
|
@@ -248,6 +277,7 @@ module Herringbone
|
|
|
248
277
|
#
|
|
249
278
|
# @param num_rows [Integer] rows in the row group
|
|
250
279
|
# @param copies [Hash{Integer => CopiedChunk}] column index => chunk to copy instead of encoding
|
|
280
|
+
# (only plaintext chunks, into columns this writer does not encrypt)
|
|
251
281
|
# @param codecs [Hash{Integer => Integer}] column index => codec id for an encoded chunk, instead
|
|
252
282
|
# of the +compression:+ option
|
|
253
283
|
# @param bloom_filters [Hash{Integer => Boolean}] column index => true to give an encoded chunk a
|
|
@@ -257,6 +287,9 @@ module Herringbone
|
|
|
257
287
|
# @raise [Error] when the writer is closed
|
|
258
288
|
def write_row_group(num_rows, copies: {}, codecs: {}, bloom_filters: {}, sorting_columns: nil)
|
|
259
289
|
raise Error, "Writer is closed" if @closed
|
|
290
|
+
if @encryption && (encrypted = copies.keys.find { |i| @encryption.encrypted?(i) })
|
|
291
|
+
raise ArgumentError, "Cannot copy a chunk into the encrypted column #{@schema.columns[encrypted].dotted_path}"
|
|
292
|
+
end
|
|
260
293
|
start = @pos
|
|
261
294
|
chunks = @schema.columns.map do |col|
|
|
262
295
|
if (copy = copies[col.index])
|
|
@@ -281,6 +314,12 @@ module Herringbone
|
|
|
281
314
|
reset_buffers
|
|
282
315
|
end
|
|
283
316
|
|
|
317
|
+
# Internal (used by Redaction): whether the column is encrypted in this file
|
|
318
|
+
#
|
|
319
|
+
# @param index [Integer] leaf column index
|
|
320
|
+
# @return [Boolean]
|
|
321
|
+
def encrypted_column?(index) = @encryption&.encrypted?(index) || false
|
|
322
|
+
|
|
284
323
|
# Internal (used by Redaction): shreds one value per row of a top-level field into the buffers,
|
|
285
324
|
# for #write_row_group. Other fields are left as they are, so the caller decides which columns
|
|
286
325
|
# are encoded and which are copied.
|
|
@@ -307,6 +346,7 @@ module Herringbone
|
|
|
307
346
|
return if @closed
|
|
308
347
|
flush_row_group
|
|
309
348
|
write_page_indexes
|
|
349
|
+
@encryption&.finish(@row_groups)
|
|
310
350
|
meta = Format::FileMetaData.new(
|
|
311
351
|
version: 2,
|
|
312
352
|
schema: @schema.to_elements,
|
|
@@ -316,10 +356,10 @@ module Herringbone
|
|
|
316
356
|
created_by: "herringbone-ruby #{VERSION}",
|
|
317
357
|
column_orders: @schema.columns.map { Format::ColumnOrder.new(type_order: Format::TypeDefinedOrder.new) }
|
|
318
358
|
)
|
|
319
|
-
footer = meta.encode
|
|
359
|
+
footer = @encryption ? @encryption.footer(meta) : meta.encode
|
|
320
360
|
write_raw(footer)
|
|
321
361
|
write_raw([footer.bytesize].pack("V"))
|
|
322
|
-
write_raw(MAGIC)
|
|
362
|
+
write_raw(@encryption ? @encryption.magic : MAGIC)
|
|
323
363
|
@io.flush if @io.respond_to?(:flush)
|
|
324
364
|
@closed = true
|
|
325
365
|
end
|
|
@@ -637,6 +677,7 @@ module Herringbone
|
|
|
637
677
|
@encodings[path] || E::PLAIN
|
|
638
678
|
end
|
|
639
679
|
|
|
680
|
+
crypto = @encryption&.chunk(@row_groups.size, col.index)
|
|
640
681
|
chunk_start = @pos
|
|
641
682
|
uncompressed_total = 0
|
|
642
683
|
dictionary_offset = nil
|
|
@@ -647,7 +688,7 @@ module Herringbone
|
|
|
647
688
|
type: Format::PageType::DICTIONARY_PAGE,
|
|
648
689
|
dictionary_page_header: Format::DictionaryPageHeader.new(num_values: dict_values.size, encoding: E::PLAIN)
|
|
649
690
|
)
|
|
650
|
-
uncompressed_total += write_page(header, plain, codec: codec)
|
|
691
|
+
uncompressed_total += write_page(header, plain, codec: codec, crypto: crypto)
|
|
651
692
|
end
|
|
652
693
|
|
|
653
694
|
data_offset = @pos
|
|
@@ -680,11 +721,12 @@ module Herringbone
|
|
|
680
721
|
encoded = encode_values(page_values, value_encoding, type, col.type_length, dict_values&.size)
|
|
681
722
|
rep_bytes = max_rep.positive? ? Encodings::RLE.encode_hybrid(reps, max_rep.bit_length) : "".b
|
|
682
723
|
def_bytes = max_def.positive? ? Encodings::RLE.encode_hybrid(defs, max_def.bit_length) : "".b
|
|
724
|
+
page_crypto = crypto && [crypto, pages.size]
|
|
683
725
|
uncompressed_total += if @data_page_version == 1
|
|
684
|
-
write_data_page_v1(n, rep_bytes, def_bytes, encoded, value_encoding, codec)
|
|
726
|
+
write_data_page_v1(n, rep_bytes, def_bytes, encoded, value_encoding, codec, page_crypto)
|
|
685
727
|
else
|
|
686
728
|
num_rows = max_rep.zero? ? n : reps.count(0)
|
|
687
|
-
write_data_page_v2(n, n - non_null, num_rows, rep_bytes, def_bytes, encoded, value_encoding, codec)
|
|
729
|
+
write_data_page_v2(n, n - non_null, num_rows, rep_bytes, def_bytes, encoded, value_encoding, codec, page_crypto)
|
|
688
730
|
end
|
|
689
731
|
pages << PageInfo.new(page_offset, @pos - page_offset, first_row, n - non_null, non_null, range)
|
|
690
732
|
first_row += max_rep.zero? ? n : reps.count(0)
|
|
@@ -707,9 +749,9 @@ module Herringbone
|
|
|
707
749
|
)
|
|
708
750
|
chunk = Format::ColumnChunk.new(file_offset: chunk_start, meta_data: meta)
|
|
709
751
|
if bloom
|
|
710
|
-
@pending_bloom_filters << [meta, build_bloom_filter(col, bloom, dict_values, values)]
|
|
752
|
+
@pending_bloom_filters << [meta, build_bloom_filter(col, bloom, dict_values, values), crypto]
|
|
711
753
|
end
|
|
712
|
-
@page_indexes << [chunk, column_index_for(col, pages, order), offset_index_for(pages)]
|
|
754
|
+
@page_indexes << [chunk, column_index_for(col, pages, order), offset_index_for(pages), crypto]
|
|
713
755
|
chunk
|
|
714
756
|
end
|
|
715
757
|
|
|
@@ -805,19 +847,22 @@ module Herringbone
|
|
|
805
847
|
|
|
806
848
|
# Page indexes go after the last row group: all column indexes, then all offset indexes.
|
|
807
849
|
# A copied chunk brings its ColumnIndex already encoded, and may come without either index.
|
|
850
|
+
# Those of encrypted columns are encrypted.
|
|
808
851
|
#
|
|
809
852
|
# @return [void]
|
|
810
853
|
def write_page_indexes
|
|
811
|
-
@page_indexes.each do |chunk, column_index, _|
|
|
854
|
+
@page_indexes.each do |chunk, column_index, _, crypto|
|
|
812
855
|
next unless column_index
|
|
813
856
|
bytes = column_index.is_a?(String) ? column_index : column_index.encode
|
|
857
|
+
bytes = crypto.encrypt(Encryption::COLUMN_INDEX, bytes) if crypto
|
|
814
858
|
chunk.column_index_offset = @pos
|
|
815
859
|
chunk.column_index_length = bytes.bytesize
|
|
816
860
|
write_raw(bytes)
|
|
817
861
|
end
|
|
818
|
-
@page_indexes.each do |chunk, _, offset_index|
|
|
862
|
+
@page_indexes.each do |chunk, _, offset_index, crypto|
|
|
819
863
|
next unless offset_index
|
|
820
864
|
bytes = offset_index.encode
|
|
865
|
+
bytes = crypto.encrypt(Encryption::OFFSET_INDEX, bytes) if crypto
|
|
821
866
|
chunk.offset_index_offset = @pos
|
|
822
867
|
chunk.offset_index_length = bytes.bytesize
|
|
823
868
|
write_raw(bytes)
|
|
@@ -893,12 +938,18 @@ module Herringbone
|
|
|
893
938
|
end
|
|
894
939
|
|
|
895
940
|
# Bloom filters go right after the row group's column chunks, in column order. A copied
|
|
896
|
-
# chunk brings its filter already encoded.
|
|
941
|
+
# chunk brings its filter already encoded. In an encrypted column the header and the bitset
|
|
942
|
+
# are encrypted separately.
|
|
897
943
|
#
|
|
898
944
|
# @return [void]
|
|
899
945
|
def write_bloom_filters
|
|
900
|
-
@pending_bloom_filters.each do |meta, filter|
|
|
901
|
-
bytes =
|
|
946
|
+
@pending_bloom_filters.each do |meta, filter, crypto|
|
|
947
|
+
bytes = if crypto
|
|
948
|
+
crypto.encrypt(Encryption::BLOOM_FILTER_HEADER, filter.header.encode) <<
|
|
949
|
+
crypto.encrypt(Encryption::BLOOM_FILTER_BITSET, filter.bitset)
|
|
950
|
+
else
|
|
951
|
+
filter.is_a?(String) ? filter : filter.encode
|
|
952
|
+
end
|
|
902
953
|
meta.bloom_filter_offset = @pos
|
|
903
954
|
meta.bloom_filter_length = bytes.bytesize
|
|
904
955
|
write_raw(bytes)
|
|
@@ -1015,18 +1066,40 @@ module Herringbone
|
|
|
1015
1066
|
|
|
1016
1067
|
# Writes a page, returning the uncompressed size including the header
|
|
1017
1068
|
#
|
|
1069
|
+
# Compresses with the configured level when +codec+ is the writer's own; chunks a redaction
|
|
1070
|
+
# keeps in their source codec use that codec's default
|
|
1071
|
+
#
|
|
1072
|
+
# @param codec [Integer] codec id
|
|
1073
|
+
# @param data [String] bytes to compress
|
|
1074
|
+
# @return [String] compressed bytes
|
|
1075
|
+
def compress(codec, data)
|
|
1076
|
+
Compression.compress(codec, data, (codec == @codec) ? @compression_level : nil)
|
|
1077
|
+
end
|
|
1078
|
+
|
|
1079
|
+
# In an encrypted column the body and the header are encrypted; the CRC covers the body as
|
|
1080
|
+
# stored, encrypted.
|
|
1081
|
+
#
|
|
1018
1082
|
# @param header [Format::PageHeader] header; sizes and CRC32 are filled in here
|
|
1019
1083
|
# @param body [String] uncompressed page body
|
|
1020
1084
|
# @param compressed [String, nil] bytes to write as the page body, when already prepared (v2 data
|
|
1021
1085
|
# pages, whose levels stay uncompressed); +body+ is compressed otherwise
|
|
1022
1086
|
# @param codec [Integer] codec id to compress +body+ with
|
|
1087
|
+
# @param crypto [Encryption::ModuleCrypto, nil] encryption of the column's modules
|
|
1088
|
+
# @param ordinal [Integer, nil] data page ordinal within the chunk; nil for a dictionary page
|
|
1023
1089
|
# @return [Integer]
|
|
1024
|
-
def write_page(header, body, compressed = nil, codec: @codec)
|
|
1025
|
-
compressed ||=
|
|
1090
|
+
def write_page(header, body, compressed = nil, codec: @codec, crypto: nil, ordinal: nil)
|
|
1091
|
+
compressed ||= compress(codec, body)
|
|
1026
1092
|
header.uncompressed_page_size ||= body.bytesize
|
|
1093
|
+
dictionary = header.type == Format::PageType::DICTIONARY_PAGE
|
|
1094
|
+
if crypto
|
|
1095
|
+
compressed = crypto.encrypt(dictionary ? Encryption::DICTIONARY_PAGE : Encryption::DATA_PAGE, compressed, ordinal)
|
|
1096
|
+
end
|
|
1027
1097
|
header.compressed_page_size = compressed.bytesize
|
|
1028
1098
|
header.crc = Zlib.crc32(compressed).then { |c| (c >= 0x8000_0000) ? c - 0x1_0000_0000 : c }
|
|
1029
1099
|
encoded = header.encode
|
|
1100
|
+
if crypto
|
|
1101
|
+
encoded = crypto.encrypt(dictionary ? Encryption::DICTIONARY_PAGE_HEADER : Encryption::DATA_PAGE_HEADER, encoded, ordinal)
|
|
1102
|
+
end
|
|
1030
1103
|
write_raw(encoded)
|
|
1031
1104
|
write_raw(compressed)
|
|
1032
1105
|
encoded.bytesize + header.uncompressed_page_size
|
|
@@ -1040,8 +1113,10 @@ module Herringbone
|
|
|
1040
1113
|
# @param encoded [String] encoded values
|
|
1041
1114
|
# @param encoding [Integer] encoding id of the values
|
|
1042
1115
|
# @param codec [Integer] codec id to compress the page with
|
|
1116
|
+
# @param page_crypto [Array(Encryption::ModuleCrypto, Integer), nil] encryption of the column's
|
|
1117
|
+
# modules and the page's ordinal, for an encrypted column
|
|
1043
1118
|
# @return [Integer] uncompressed size including the header
|
|
1044
|
-
def write_data_page_v1(n, rep_bytes, def_bytes, encoded, encoding, codec)
|
|
1119
|
+
def write_data_page_v1(n, rep_bytes, def_bytes, encoded, encoding, codec, page_crypto)
|
|
1045
1120
|
body = String.new(encoding: Encoding::BINARY)
|
|
1046
1121
|
body << [rep_bytes.bytesize].pack("V") << rep_bytes unless rep_bytes.empty?
|
|
1047
1122
|
body << [def_bytes.bytesize].pack("V") << def_bytes unless def_bytes.empty?
|
|
@@ -1053,7 +1128,7 @@ module Herringbone
|
|
|
1053
1128
|
definition_level_encoding: E::RLE, repetition_level_encoding: E::RLE
|
|
1054
1129
|
)
|
|
1055
1130
|
)
|
|
1056
|
-
write_page(header, body, codec: codec)
|
|
1131
|
+
write_page(header, body, codec: codec, crypto: page_crypto&.first, ordinal: page_crypto&.last)
|
|
1057
1132
|
end
|
|
1058
1133
|
|
|
1059
1134
|
# A DATA_PAGE_V2: levels without length prefixes and uncompressed, then the compressed values
|
|
@@ -1066,9 +1141,11 @@ module Herringbone
|
|
|
1066
1141
|
# @param encoded [String] encoded values
|
|
1067
1142
|
# @param encoding [Integer] encoding id of the values
|
|
1068
1143
|
# @param codec [Integer] codec id to compress the values with
|
|
1144
|
+
# @param page_crypto [Array(Encryption::ModuleCrypto, Integer), nil] encryption of the column's
|
|
1145
|
+
# modules and the page's ordinal, for an encrypted column
|
|
1069
1146
|
# @return [Integer] uncompressed size including the header
|
|
1070
|
-
def write_data_page_v2(n, nulls, rows, rep_bytes, def_bytes, encoded, encoding, codec)
|
|
1071
|
-
compressed =
|
|
1147
|
+
def write_data_page_v2(n, nulls, rows, rep_bytes, def_bytes, encoded, encoding, codec, page_crypto)
|
|
1148
|
+
compressed = compress(codec, encoded)
|
|
1072
1149
|
header = Format::PageHeader.new(
|
|
1073
1150
|
type: Format::PageType::DATA_PAGE_V2,
|
|
1074
1151
|
uncompressed_page_size: rep_bytes.bytesize + def_bytes.bytesize + encoded.bytesize,
|
|
@@ -1079,7 +1156,8 @@ module Herringbone
|
|
|
1079
1156
|
is_compressed: codec != Format::Codec::UNCOMPRESSED
|
|
1080
1157
|
)
|
|
1081
1158
|
)
|
|
1082
|
-
write_page(header, "".b, rep_bytes + def_bytes + compressed, codec: codec
|
|
1159
|
+
write_page(header, "".b, rep_bytes + def_bytes + compressed, codec: codec, crypto: page_crypto&.first,
|
|
1160
|
+
ordinal: page_crypto&.last)
|
|
1083
1161
|
end
|
|
1084
1162
|
|
|
1085
1163
|
# Statistics and column index bounds longer than this are truncated, see #truncate_min
|