blob: e77cf59ac3d03d84a99f6856f34a775d9e6c9ca3 [file] [log] [blame]
Amin Hassanic3e6b532017-03-07 17:47:25 -08001// Copyright 2017 The Chromium OS Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "puffin/src/include/puffin/utils.h"
6
7#include <inttypes.h>
8
Sen Jiang5eb33e82018-05-01 15:01:11 -07009#include <algorithm>
10#include <set>
Amin Hassanic3e6b532017-03-07 17:47:25 -080011#include <string>
12#include <vector>
13
14#include "puffin/src/bit_reader.h"
Amin Hassani00f08322017-10-18 11:55:10 -070015#include "puffin/src/file_stream.h"
Amin Hassanic3e6b532017-03-07 17:47:25 -080016#include "puffin/src/include/puffin/common.h"
Amin Hassani26bcfdd2017-09-29 17:54:15 -070017#include "puffin/src/include/puffin/puffer.h"
Amin Hassanie2e9cb02018-03-15 14:14:58 -070018#include "puffin/src/logging.h"
Tianjie Xu11942232018-01-18 18:31:42 -080019#include "puffin/src/memory_stream.h"
Amin Hassani26bcfdd2017-09-29 17:54:15 -070020#include "puffin/src/puff_writer.h"
Amin Hassanic3e6b532017-03-07 17:47:25 -080021
Sen Jiang5eb33e82018-05-01 15:01:11 -070022using std::set;
Amin Hassani10b869c2018-03-15 13:22:32 -070023using std::string;
24using std::vector;
25
Tianjie Xu11942232018-01-18 18:31:42 -080026namespace {
27// Use memcpy to access the unaligned data of type |T|.
28template <typename T>
29inline T get_unaligned(const void* address) {
30 T result;
31 memcpy(&result, address, sizeof(T));
32 return result;
33}
34
Sen Jiang5eb33e82018-05-01 15:01:11 -070035struct ExtentData {
36 puffin::BitExtent extent;
37 uint64_t byte_offset;
38 uint64_t byte_length;
39 const puffin::Buffer& data;
40
41 ExtentData(const puffin::BitExtent& in_extent, const puffin::Buffer& in_data)
42 : extent(in_extent), data(in_data) {
43 // Round start offset up and end offset down to exclude bits not in this
44 // extent. We simply ignore the bits at start and end that's not on byte
45 // boundary because as long as the majority of the bytes are the same,
46 // bsdiff will be able to reference it.
47 byte_offset = (extent.offset + 7) / 8;
48 uint64_t byte_end_offset = (extent.offset + extent.length) / 8;
49 CHECK(byte_end_offset <= data.size());
50 if (byte_end_offset > byte_offset) {
51 byte_length = byte_end_offset - byte_offset;
52 } else {
53 byte_length = 0;
54 }
55 }
56
57 int Compare(const ExtentData& other) const {
58 if (extent.length != other.extent.length) {
59 return extent.length < other.extent.length ? -1 : 1;
60 }
61 return memcmp(data.data() + byte_offset,
62 other.data.data() + other.byte_offset,
63 std::min(byte_length, other.byte_length));
64 }
65 bool operator<(const ExtentData& other) const { return Compare(other) < 0; }
66 bool operator==(const ExtentData& other) const { return Compare(other) == 0; }
67};
68
Tianjie Xu11942232018-01-18 18:31:42 -080069} // namespace
70
Amin Hassanic3e6b532017-03-07 17:47:25 -080071namespace puffin {
72
Amin Hassani8d0ec652018-06-05 16:02:28 -070073bool LocateDeflatesInDeflateStream(const uint8_t* data,
74 uint64_t size,
75 uint64_t virtual_offset,
76 vector<BitExtent>* deflates,
77 uint64_t* compressed_size) {
78 Puffer puffer;
79 BufferBitReader bit_reader(data, size);
80 BufferPuffWriter puff_writer(nullptr, 0);
81 vector<BitExtent> sub_deflates;
82 TEST_AND_RETURN_FALSE(
83 puffer.PuffDeflate(&bit_reader, &puff_writer, &sub_deflates));
84 for (const auto& deflate : sub_deflates) {
85 deflates->emplace_back(deflate.offset + virtual_offset * 8, deflate.length);
86 }
87 if (compressed_size) {
88 *compressed_size = bit_reader.Offset();
89 }
90 return true;
91}
92
Amin Hassanic3e6b532017-03-07 17:47:25 -080093// This function uses RFC1950 (https://www.ietf.org/rfc/rfc1950.txt) for the
Amin Hassani75a7f2c2018-02-21 11:51:28 -080094// definition of a zlib stream. For finding the deflate blocks, we relying on
95// the proper size of the zlib stream in |data|. Basically the size of the zlib
96// stream should be known before hand. Otherwise we need to parse the stream and
97// find the location of compressed blocks using CalculateSizeOfDeflateBlock().
98bool LocateDeflatesInZlib(const Buffer& data,
Amin Hassani8d0ec652018-06-05 16:02:28 -070099 std::vector<BitExtent>* deflates) {
Amin Hassani75a7f2c2018-02-21 11:51:28 -0800100 // A zlib stream has the following format:
101 // 0 1 compression method and flag
102 // 1 1 flag
103 // 2 4 preset dictionary (optional)
104 // 2 or 6 n compressed data
105 // n+(2 or 6) 4 Adler-32 checksum
106 TEST_AND_RETURN_FALSE(data.size() >= 6 + 4); // Header + Footer
107 uint16_t cmf = data[0];
108 auto compression_method = cmf & 0x0F;
109 // For deflate compression_method should be 8.
110 TEST_AND_RETURN_FALSE(compression_method == 8);
Amin Hassanic3e6b532017-03-07 17:47:25 -0800111
Amin Hassani75a7f2c2018-02-21 11:51:28 -0800112 auto cinfo = (cmf & 0xF0) >> 4;
113 // Value greater than 7 is not allowed in deflate.
114 TEST_AND_RETURN_FALSE(cinfo <= 7);
Amin Hassanic3e6b532017-03-07 17:47:25 -0800115
Amin Hassani75a7f2c2018-02-21 11:51:28 -0800116 auto flag = data[1];
117 TEST_AND_RETURN_FALSE(((cmf << 8) + flag) % 31 == 0);
Amin Hassanic3e6b532017-03-07 17:47:25 -0800118
Amin Hassanid7768d52018-02-28 15:34:21 -0800119 uint64_t header_len = 2;
Amin Hassani75a7f2c2018-02-21 11:51:28 -0800120 if (flag & 0x20) {
121 header_len += 4; // 4 bytes for the preset dictionary.
Amin Hassani7074da62017-09-30 17:14:06 -0700122 }
Amin Hassani75a7f2c2018-02-21 11:51:28 -0800123
124 // 4 is for ADLER32.
Amin Hassani8d0ec652018-06-05 16:02:28 -0700125 TEST_AND_RETURN_FALSE(LocateDeflatesInDeflateStream(
126 data.data() + header_len, data.size() - header_len - 4, header_len,
127 deflates, nullptr));
Amin Hassani7074da62017-09-30 17:14:06 -0700128 return true;
129}
130
131bool FindDeflateSubBlocks(const UniqueStreamPtr& src,
132 const vector<ByteExtent>& deflates,
133 vector<BitExtent>* subblock_deflates) {
134 Puffer puffer;
135 Buffer deflate_buffer;
136 for (const auto& deflate : deflates) {
137 TEST_AND_RETURN_FALSE(src->Seek(deflate.offset));
138 // Read from src into deflate_buffer.
139 deflate_buffer.resize(deflate.length);
140 TEST_AND_RETURN_FALSE(src->Read(deflate_buffer.data(), deflate.length));
141
142 // Find all the subblocks.
143 BufferBitReader bit_reader(deflate_buffer.data(), deflate.length);
Amin Hassanib8325c22018-05-22 14:57:22 -0700144 // The uncompressed blocks will be ignored since we are passing a null
145 // buffered puff writer and a valid deflate locations output array. This
146 // should not happen in the puffdiff or anywhere else by default.
Amin Hassani7074da62017-09-30 17:14:06 -0700147 BufferPuffWriter puff_writer(nullptr, 0);
Amin Hassani7074da62017-09-30 17:14:06 -0700148 vector<BitExtent> subblocks;
149 TEST_AND_RETURN_FALSE(
Amin Hassanie2e9cb02018-03-15 14:14:58 -0700150 puffer.PuffDeflate(&bit_reader, &puff_writer, &subblocks));
Amin Hassani7074da62017-09-30 17:14:06 -0700151 TEST_AND_RETURN_FALSE(deflate.length == bit_reader.Offset());
152 for (const auto& subblock : subblocks) {
153 subblock_deflates->emplace_back(subblock.offset + deflate.offset * 8,
154 subblock.length);
155 }
Amin Hassanic3e6b532017-03-07 17:47:25 -0800156 }
157 return true;
158}
159
Amin Hassani00f08322017-10-18 11:55:10 -0700160bool LocateDeflatesInZlibBlocks(const string& file_path,
161 const vector<ByteExtent>& zlibs,
162 vector<BitExtent>* deflates) {
163 auto src = FileStream::Open(file_path, true, false);
164 TEST_AND_RETURN_FALSE(src);
Amin Hassani75a7f2c2018-02-21 11:51:28 -0800165
166 Buffer buffer;
Amin Hassani8d0ec652018-06-05 16:02:28 -0700167 for (const auto& zlib : zlibs) {
Amin Hassani75a7f2c2018-02-21 11:51:28 -0800168 buffer.resize(zlib.length);
169 TEST_AND_RETURN_FALSE(src->Seek(zlib.offset));
170 TEST_AND_RETURN_FALSE(src->Read(buffer.data(), buffer.size()));
Amin Hassani8d0ec652018-06-05 16:02:28 -0700171 vector<BitExtent> tmp_deflates;
172 TEST_AND_RETURN_FALSE(LocateDeflatesInZlib(buffer, &tmp_deflates));
173 for (const auto& deflate : tmp_deflates) {
174 deflates->emplace_back(deflate.offset + zlib.offset * 8, deflate.length);
Amin Hassani75a7f2c2018-02-21 11:51:28 -0800175 }
176 }
177 return true;
Amin Hassani00f08322017-10-18 11:55:10 -0700178}
179
Amin Hassani4a212ed2018-02-15 10:31:28 -0800180// For more information about gzip format, refer to RFC 1952 located at:
181// https://www.ietf.org/rfc/rfc1952.txt
Amin Hassani8d0ec652018-06-05 16:02:28 -0700182bool LocateDeflatesInGzip(const Buffer& data, vector<BitExtent>* deflates) {
Amin Hassanid7768d52018-02-28 15:34:21 -0800183 uint64_t member_start = 0;
Sen Jiang6d30f042018-11-12 19:12:10 -0800184 while (member_start + 10 <= data.size() && data[member_start + 0] == 0x1F &&
185 data[member_start + 1] == 0x8B && data[member_start + 2] == 8) {
Amin Hassani4a212ed2018-02-15 10:31:28 -0800186 // Each member entry has the following format
187 // 0 1 0x1F
188 // 1 1 0x8B
189 // 2 1 compression method (8 denotes deflate)
190 // 3 1 set of flags
191 // 4 4 modification time
192 // 8 1 extra flags
193 // 9 1 operating system
Amin Hassani4a212ed2018-02-15 10:31:28 -0800194
Amin Hassanid7768d52018-02-28 15:34:21 -0800195 uint64_t offset = member_start + 10;
Amin Hassani4a212ed2018-02-15 10:31:28 -0800196 int flag = data[member_start + 3];
197 // Extra field
198 if (flag & 4) {
199 TEST_AND_RETURN_FALSE(offset + 2 <= data.size());
200 uint16_t extra_length = data[offset++];
201 extra_length |= static_cast<uint16_t>(data[offset++]) << 8;
202 TEST_AND_RETURN_FALSE(offset + extra_length <= data.size());
203 offset += extra_length;
204 }
205 // File name field
206 if (flag & 8) {
207 while (true) {
208 TEST_AND_RETURN_FALSE(offset + 1 <= data.size());
209 if (data[offset++] == 0) {
210 break;
211 }
212 }
213 }
214 // File comment field
215 if (flag & 16) {
216 while (true) {
217 TEST_AND_RETURN_FALSE(offset + 1 <= data.size());
218 if (data[offset++] == 0) {
219 break;
220 }
221 }
222 }
223 // CRC16 field
224 if (flag & 2) {
225 offset += 2;
226 }
227
Amin Hassani8d0ec652018-06-05 16:02:28 -0700228 uint64_t compressed_size = 0;
229 TEST_AND_RETURN_FALSE(LocateDeflatesInDeflateStream(
230 data.data() + offset, data.size() - offset, offset, deflates,
231 &compressed_size));
Amin Hassani4a212ed2018-02-15 10:31:28 -0800232 offset += compressed_size;
233
Amin Hassani8d0ec652018-06-05 16:02:28 -0700234 // Ignore CRC32 and uncompressed size.
Amin Hassani4a212ed2018-02-15 10:31:28 -0800235 TEST_AND_RETURN_FALSE(offset + 8 <= data.size());
Amin Hassani8d0ec652018-06-05 16:02:28 -0700236 offset += 8;
Amin Hassani4a212ed2018-02-15 10:31:28 -0800237 member_start = offset;
238 }
Sen Jiang6d30f042018-11-12 19:12:10 -0800239 // Return true if we've successfully parsed at least one gzip.
240 return member_start != 0;
Amin Hassani4a212ed2018-02-15 10:31:28 -0800241}
242
Tianjie Xu11942232018-01-18 18:31:42 -0800243// For more information about the zip format, refer to
244// https://support.pkware.com/display/PKZIP/APPNOTE
245bool LocateDeflatesInZipArchive(const Buffer& data,
Amin Hassani8d0ec652018-06-05 16:02:28 -0700246 vector<BitExtent>* deflates) {
Amin Hassanid7768d52018-02-28 15:34:21 -0800247 uint64_t pos = 0;
Tianjie Xu11942232018-01-18 18:31:42 -0800248 while (pos <= data.size() - 30) {
249 // TODO(xunchang) add support for big endian system when searching for
250 // magic numbers.
251 if (get_unaligned<uint32_t>(data.data() + pos) != 0x04034b50) {
252 pos++;
253 continue;
254 }
255
256 // local file header format
257 // 0 4 0x04034b50
258 // 4 2 minimum version needed to extract
259 // 6 2 general purpose bit flag
260 // 8 2 compression method
261 // 10 4 file last modification date & time
262 // 14 4 CRC-32
263 // 18 4 compressed size
264 // 22 4 uncompressed size
265 // 26 2 file name length
266 // 28 2 extra field length
267 // 30 n file name
268 // 30+n m extra field
269 auto compression_method = get_unaligned<uint16_t>(data.data() + pos + 8);
270 if (compression_method != 8) { // non-deflate type
271 pos += 4;
272 continue;
273 }
274
275 auto compressed_size = get_unaligned<uint32_t>(data.data() + pos + 18);
Tianjie Xu11942232018-01-18 18:31:42 -0800276 auto file_name_length = get_unaligned<uint16_t>(data.data() + pos + 26);
277 auto extra_field_length = get_unaligned<uint16_t>(data.data() + pos + 28);
278 uint64_t header_size = 30 + file_name_length + extra_field_length;
279
280 // sanity check
281 if (static_cast<uint64_t>(header_size) + compressed_size > data.size() ||
282 pos > data.size() - header_size - compressed_size) {
283 pos += 4;
284 continue;
285 }
286
Amin Hassani8d0ec652018-06-05 16:02:28 -0700287 vector<BitExtent> tmp_deflates;
288 uint64_t offset = pos + header_size;
289 uint64_t calculated_compressed_size = 0;
290 if (!LocateDeflatesInDeflateStream(
291 data.data() + offset, data.size() - offset, offset, &tmp_deflates,
292 &calculated_compressed_size)) {
Tianjie Xu11942232018-01-18 18:31:42 -0800293 LOG(ERROR) << "Failed to decompress the zip entry starting from: " << pos
294 << ", skip adding deflates for this entry.";
295 pos += 4;
296 continue;
297 }
298
Amin Hassani8d0ec652018-06-05 16:02:28 -0700299 // Double check the compressed size if it is available in the file header.
Tianjie Xu11942232018-01-18 18:31:42 -0800300 if (compressed_size > 0 && compressed_size != calculated_compressed_size) {
301 LOG(WARNING) << "Compressed size in the file header: " << compressed_size
302 << " doesn't equal the real size: "
303 << calculated_compressed_size;
304 }
305
Amin Hassani8d0ec652018-06-05 16:02:28 -0700306 deflates->insert(deflates->end(), tmp_deflates.begin(), tmp_deflates.end());
Tianjie Xu11942232018-01-18 18:31:42 -0800307 pos += header_size + calculated_compressed_size;
308 }
309
310 return true;
311}
312
313bool LocateDeflateSubBlocksInZipArchive(const Buffer& data,
314 vector<BitExtent>* deflates) {
Amin Hassani8d0ec652018-06-05 16:02:28 -0700315 return LocateDeflatesInZipArchive(data, deflates);
Tianjie Xu11942232018-01-18 18:31:42 -0800316}
317
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700318bool FindPuffLocations(const UniqueStreamPtr& src,
Amin Hassani7074da62017-09-30 17:14:06 -0700319 const vector<BitExtent>& deflates,
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700320 vector<ByteExtent>* puffs,
Amin Hassanid7768d52018-02-28 15:34:21 -0800321 uint64_t* out_puff_size) {
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700322 Puffer puffer;
323 Buffer deflate_buffer;
324
325 // Here accumulate the size difference between each corresponding deflate and
326 // puff. At the end we add this cummulative size difference to the size of the
327 // deflate stream to get the size of the puff stream. We use signed size
328 // because puff size could be smaller than deflate size.
Amin Hassanid7768d52018-02-28 15:34:21 -0800329 int64_t total_size_difference = 0;
Amin Hassani7074da62017-09-30 17:14:06 -0700330 for (auto deflate = deflates.begin(); deflate != deflates.end(); ++deflate) {
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700331 // Read from src into deflate_buffer.
Amin Hassani7074da62017-09-30 17:14:06 -0700332 auto start_byte = deflate->offset / 8;
333 auto end_byte = (deflate->offset + deflate->length + 7) / 8;
334 deflate_buffer.resize(end_byte - start_byte);
335 TEST_AND_RETURN_FALSE(src->Seek(start_byte));
336 TEST_AND_RETURN_FALSE(
337 src->Read(deflate_buffer.data(), deflate_buffer.size()));
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700338 // Find the size of the puff.
Amin Hassani7074da62017-09-30 17:14:06 -0700339 BufferBitReader bit_reader(deflate_buffer.data(), deflate_buffer.size());
Amin Hassanid7768d52018-02-28 15:34:21 -0800340 uint64_t bits_to_skip = deflate->offset % 8;
Amin Hassani7074da62017-09-30 17:14:06 -0700341 TEST_AND_RETURN_FALSE(bit_reader.CacheBits(bits_to_skip));
342 bit_reader.DropBits(bits_to_skip);
343
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700344 BufferPuffWriter puff_writer(nullptr, 0);
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700345 TEST_AND_RETURN_FALSE(
Amin Hassanie2e9cb02018-03-15 14:14:58 -0700346 puffer.PuffDeflate(&bit_reader, &puff_writer, nullptr));
Amin Hassani7074da62017-09-30 17:14:06 -0700347 TEST_AND_RETURN_FALSE(deflate_buffer.size() == bit_reader.Offset());
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700348
Amin Hassani7074da62017-09-30 17:14:06 -0700349 // 1 if a deflate ends at the same byte that the next deflate starts and
350 // there is a few bits gap between them. In practice this may never happen,
351 // but it is a good idea to support it anyways. If there is a gap, the value
352 // of the gap will be saved as an integer byte to the puff stream. The parts
353 // of the byte that belogs to the deflates are shifted out.
354 int gap = 0;
355 if (deflate != deflates.begin()) {
356 auto prev_deflate = std::prev(deflate);
357 if ((prev_deflate->offset + prev_deflate->length == deflate->offset)
358 // If deflates are on byte boundary the gap will not be counted later,
359 // so we won't worry about it.
360 && (deflate->offset % 8 != 0)) {
361 gap = 1;
362 }
363 }
364
365 start_byte = ((deflate->offset + 7) / 8);
366 end_byte = (deflate->offset + deflate->length) / 8;
Amin Hassanid7768d52018-02-28 15:34:21 -0800367 int64_t deflate_length_in_bytes = end_byte - start_byte;
Amin Hassani7074da62017-09-30 17:14:06 -0700368
369 // If there was no gap bits between the current and previous deflates, there
370 // will be no extra gap byte, so the offset will be shifted one byte back.
371 auto puff_offset = start_byte - gap + total_size_difference;
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700372 auto puff_size = puff_writer.Size();
Amin Hassani7074da62017-09-30 17:14:06 -0700373 // Add the location into puff.
374 puffs->emplace_back(puff_offset, puff_size);
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700375 total_size_difference +=
Amin Hassanid7768d52018-02-28 15:34:21 -0800376 static_cast<int64_t>(puff_size) - deflate_length_in_bytes - gap;
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700377 }
378
Amin Hassanid7768d52018-02-28 15:34:21 -0800379 uint64_t src_size;
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700380 TEST_AND_RETURN_FALSE(src->GetSize(&src_size));
Amin Hassanid7768d52018-02-28 15:34:21 -0800381 auto final_size = static_cast<int64_t>(src_size) + total_size_difference;
Amin Hassani26bcfdd2017-09-29 17:54:15 -0700382 TEST_AND_RETURN_FALSE(final_size >= 0);
383 *out_puff_size = final_size;
384 return true;
385}
386
Sen Jiang5eb33e82018-05-01 15:01:11 -0700387void RemoveEqualBitExtents(const Buffer& data1,
388 const Buffer& data2,
389 std::vector<BitExtent>* extents1,
390 std::vector<BitExtent>* extents2) {
391 set<ExtentData> extent1_set, equal_extents;
392 for (const BitExtent& ext : *extents1) {
393 extent1_set.emplace(ext, data1);
394 }
395
396 auto new_extents2_end = extents2->begin();
397 for (const BitExtent& ext : *extents2) {
398 ExtentData extent_data(ext, data2);
399 if (extent1_set.find(extent_data) != extent1_set.end()) {
400 equal_extents.insert(extent_data);
401 } else {
402 *new_extents2_end++ = ext;
403 }
404 }
405 extents2->erase(new_extents2_end, extents2->end());
406 extents1->erase(
407 std::remove_if(extents1->begin(), extents1->end(),
408 [&equal_extents, &data1](const BitExtent& ext) {
409 return equal_extents.find(ExtentData(ext, data1)) !=
410 equal_extents.end();
411 }),
412 extents1->end());
413}
Amin Hassanidfec7fa2019-01-03 11:43:15 -0800414
415bool RemoveDeflatesWithBadDistanceCaches(const Buffer& data,
416 vector<BitExtent>* deflates) {
417 Puffer puffer(true /* exclude_bad_distance_caches */);
418 for (auto def = deflates->begin(); def != deflates->end();) {
419 uint64_t offset = def->offset / 8;
420 uint64_t length = (def->offset + def->length + 7) / 8 - offset;
421 BufferBitReader br(&data[offset], length);
422 BufferPuffWriter pw(nullptr, 0);
423
424 // Drop the first few bits in the buffer so we start exactly where the
425 // deflate starts.
426 uint64_t bits_to_drop = def->offset % 8;
427 TEST_AND_RETURN_FALSE(br.CacheBits(bits_to_drop));
428 br.DropBits(bits_to_drop);
429
430 vector<BitExtent> defs_out;
431 TEST_AND_RETURN_FALSE(puffer.PuffDeflate(&br, &pw, &defs_out));
432
433 TEST_AND_RETURN_FALSE(defs_out.size() <= 1);
434 if (defs_out.size() == 0) {
435 // This is a deflate we were looking for, remove it.
436 def = deflates->erase(def);
437 } else {
438 ++def;
439 }
440 }
441 return true;
442}
443
Amin Hassanic3e6b532017-03-07 17:47:25 -0800444} // namespace puffin