blob: 5a3fb8a3005e0116b174286d122b7cf855d2c58f [file] [log] [blame]
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
// Some portions copyright (C) 2008, Google, inc.
//
// Utilities for working with protobufs.
// Some of this code is cribbed from the protobuf source,
// but modified to work with kudu's 'faststring' instead of STL strings.
#include "kudu/util/pb_util.h"
#include <algorithm>
#include <cstddef>
#include <deque>
#include <initializer_list>
#include <limits>
#include <memory>
#include <mutex>
#include <ostream>
#include <string>
#include <unordered_set>
#include <vector>
#include <boost/optional/optional.hpp>
#include <glog/logging.h>
#include <google/protobuf/descriptor.h>
#include <google/protobuf/descriptor.pb.h>
#include <google/protobuf/descriptor_database.h>
#include <google/protobuf/dynamic_message.h>
#include <google/protobuf/io/coded_stream.h>
#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
#include <google/protobuf/message.h>
#include <google/protobuf/message_lite.h>
#include <google/protobuf/stubs/status.h>
#include <google/protobuf/text_format.h>
#include <google/protobuf/util/json_util.h>
#include "kudu/gutil/integral_types.h"
#include "kudu/gutil/macros.h"
#include "kudu/gutil/map-util.h"
#include "kudu/gutil/port.h"
#include "kudu/gutil/strings/escaping.h"
#include "kudu/gutil/strings/fastmem.h"
#include "kudu/gutil/strings/substitute.h"
#include "kudu/util/coding.h"
#include "kudu/util/coding-inl.h"
#include "kudu/util/crc.h"
#include "kudu/util/debug/sanitizer_scopes.h"
#include "kudu/util/debug/trace_event.h"
#include "kudu/util/env.h"
#include "kudu/util/env_util.h"
#include "kudu/util/faststring.h"
#include "kudu/util/jsonwriter.h"
#include "kudu/util/logging.h"
#include "kudu/util/path_util.h"
#include "kudu/util/pb_util-internal.h"
#include "kudu/util/pb_util.pb.h"
#include "kudu/util/scoped_cleanup.h"
#include "kudu/util/slice.h"
#include "kudu/util/status.h"
using google::protobuf::Descriptor;
using google::protobuf::DescriptorPool;
using google::protobuf::DynamicMessageFactory;
using google::protobuf::FieldDescriptor;
using google::protobuf::FileDescriptor;
using google::protobuf::FileDescriptorProto;
using google::protobuf::FileDescriptorSet;
using google::protobuf::io::ArrayInputStream;
using google::protobuf::io::CodedInputStream;
using google::protobuf::Message;
using google::protobuf::MessageLite;
using google::protobuf::Reflection;
using google::protobuf::SimpleDescriptorDatabase;
using google::protobuf::TextFormat;
using kudu::crc::Crc;
using kudu::pb_util::internal::SequentialFileFileInputStream;
using kudu::pb_util::internal::WritableFileOutputStream;
using std::deque;
using std::endl;
using std::initializer_list;
using std::ostream;
using std::shared_ptr;
using std::string;
using std::unique_ptr;
using std::unordered_set;
using std::vector;
using strings::Substitute;
using strings::Utf8SafeCEscape;
namespace std {
// Allow the use of FileState with DCHECK_EQ.
std::ostream& operator<< (std::ostream& os, const kudu::pb_util::FileState& state) {
os << static_cast<int>(state);
return os;
}
} // namespace std
namespace kudu {
namespace pb_util {
static const char* const kTmpTemplateSuffix = ".XXXXXX";
// Protobuf container constants.
static const uint32_t kPBContainerInvalidVersion = 0;
static const uint32_t kPBContainerDefaultVersion = 2;
static const int kPBContainerChecksumLen = sizeof(uint32_t);
static const char kPBContainerMagic[] = "kuducntr";
static const int kPBContainerMagicLen = 8;
static const int kPBContainerV1HeaderLen =
kPBContainerMagicLen + sizeof(uint32_t); // Magic number + version.
static const int kPBContainerV2HeaderLen =
kPBContainerV1HeaderLen + kPBContainerChecksumLen; // Same as V1 plus a checksum.
const int kPBContainerMinimumValidLength = kPBContainerV1HeaderLen;
static_assert(arraysize(kPBContainerMagic) - 1 == kPBContainerMagicLen,
"kPBContainerMagic does not match expected length");
namespace {
// When serializing, we first compute the byte size, then serialize the message.
// If serialization produces a different number of bytes than expected, we
// call this function, which crashes. The problem could be due to a bug in the
// protobuf implementation but is more likely caused by concurrent modification
// of the message. This function attempts to distinguish between the two and
// provide a useful error message.
void ByteSizeConsistencyError(size_t byte_size_before_serialization,
size_t byte_size_after_serialization,
size_t bytes_produced_by_serialization) {
CHECK_EQ(byte_size_before_serialization, byte_size_after_serialization)
<< "Protocol message was modified concurrently during serialization.";
CHECK_EQ(bytes_produced_by_serialization, byte_size_before_serialization)
<< "Byte size calculation and serialization were inconsistent. This "
"may indicate a bug in protocol buffers or it may be caused by "
"concurrent modification of the message.";
LOG(FATAL) << "This shouldn't be called if all the sizes are equal.";
}
string InitializationErrorMessage(const char* action,
const MessageLite& message) {
// Note: We want to avoid depending on strutil in the lite library, otherwise
// we'd use:
//
// return strings::Substitute(
// "Can't $0 message of type \"$1\" because it is missing required "
// "fields: $2",
// action, message.GetTypeName(),
// message.InitializationErrorString());
string result;
result += "Can't ";
result += action;
result += " message of type \"";
result += message.GetTypeName();
result += "\" because it is missing required fields: ";
result += message.InitializationErrorString();
return result;
}
// Returns true iff the specified protobuf container file version is supported
// by this implementation.
bool IsSupportedContainerVersion(uint32_t version) {
if (version == 1 || version == 2) {
return true;
}
return false;
}
// Reads exactly 'length' bytes from the container file into 'result',
// validating that there is sufficient data in the file to read this length
// before attempting to do so, and validating that it has read that length
// after performing the read.
//
// If the file size is less than the requested size of the read, returns
// Status::Incomplete.
// If there is an unexpected short read, returns Status::Corruption.
//
// NOTE: the data in 'result' may be modified even in the case of a failed read.
template<typename ReadableFileType>
Status ValidateAndReadData(ReadableFileType* reader, uint64_t file_size,
uint64_t* offset, uint64_t length,
faststring* result) {
// Validate the read length using the file size.
if (*offset + length > file_size) {
return Status::Incomplete("File size not large enough to be valid",
Substitute("Proto container file $0: "
"Tried to read $1 bytes at offset "
"$2 but file size is only $3 bytes",
reader->filename(), length,
*offset, file_size));
}
// Perform the read.
result->resize(length);
RETURN_NOT_OK(reader->Read(*offset, Slice(*result)));
*offset += length;
return Status::OK();
}
// Helper macro for use with ParseAndCompareChecksum(). Example usage:
// RETURN_NOT_OK_PREPEND(ParseAndCompareChecksum(checksum.data(), { data }),
// CHECKSUM_ERR_MSG("Data checksum does not match", filename, offset));
#define CHECKSUM_ERR_MSG(prefix, filename, cksum_offset) \
Substitute("$0: Incorrect checksum in file $1 at offset $2", prefix, filename, cksum_offset)
// Parses a checksum from the specified buffer and compares it to the bytes
// given in 'slices' by calculating a rolling CRC32 checksum of the bytes in
// the 'slices'.
// If they match, returns OK. Otherwise, returns Status::Corruption.
Status ParseAndCompareChecksum(const uint8_t* checksum_buf,
const initializer_list<Slice>& slices) {
uint32_t written_checksum = DecodeFixed32(checksum_buf);
uint64_t actual_checksum = 0;
Crc* crc32c = crc::GetCrc32cInstance();
for (Slice s : slices) {
crc32c->Compute(s.data(), s.size(), &actual_checksum);
}
if (PREDICT_FALSE(actual_checksum != written_checksum)) {
return Status::Corruption(Substitute("Checksum does not match. Expected: $0. Actual: $1",
written_checksum, actual_checksum));
}
return Status::OK();
}
// If necessary, get the size of the file opened by 'reader' in 'cached_file_size'.
// If 'cached_file_size' already has a value, this is a no-op.
template<typename ReadableFileType>
Status CacheFileSize(ReadableFileType* reader,
boost::optional<uint64_t>* cached_file_size) {
if (*cached_file_size) {
return Status::OK();
}
uint64_t file_size;
RETURN_NOT_OK(reader->Size(&file_size));
*cached_file_size = file_size;
return Status::OK();
}
template<typename ReadableFileType>
Status RestOfFileIsAllZeros(ReadableFileType* reader,
uint64_t filesize,
uint64_t offset,
bool* all_zeros) {
DCHECK(reader);
DCHECK_GE(filesize, offset);
DCHECK(all_zeros);
constexpr uint64_t max_to_read = 4 * 1024 * 1024; // 4 MiB.
faststring buf;
while (true) {
uint64_t to_read = std::min(max_to_read, filesize - offset);
if (to_read == 0) {
break;
}
buf.resize(to_read);
RETURN_NOT_OK(reader->Read(offset, Slice(buf)));
offset += to_read;
if (!IsAllZeros(buf)) {
*all_zeros = false;
return Status::OK();
}
}
*all_zeros = true;
return Status::OK();
}
// Read and parse a message of the specified format at the given offset in the
// format documented in pb_util.h. 'offset' is an in-out parameter and will be
// updated with the new offset on success. On failure, 'offset' is not modified.
template<typename ReadableFileType>
Status ReadPBStartingAt(ReadableFileType* reader, int version,
boost::optional<uint64_t>* cached_file_size,
uint64_t* offset, Message* msg) {
uint64_t tmp_offset = *offset;
VLOG(1) << "Reading PB with version " << version << " starting at offset " << *offset;
RETURN_NOT_OK(CacheFileSize(reader, cached_file_size));
uint64_t file_size = cached_file_size->get();
if (tmp_offset == *cached_file_size) {
return Status::EndOfFile("Reached end of file");
}
// Read the data length from the file.
// Version 2+ includes a checksum for the length field.
uint64_t length_buflen = (version == 1) ? sizeof(uint32_t)
: sizeof(uint32_t) + kPBContainerChecksumLen;
faststring length_and_cksum_buf;
RETURN_NOT_OK_PREPEND(ValidateAndReadData(reader, file_size, &tmp_offset, length_buflen,
&length_and_cksum_buf),
Substitute("Could not read data length from proto container file $0 "
"at offset $1", reader->filename(), *offset));
Slice length(length_and_cksum_buf.data(), sizeof(uint32_t));
// Versions >= 2 have an individual checksum for the data length.
if (version >= 2) {
// KUDU-2260: If the length and checksum data are all 0's, and the rest of
// the file is all 0's, then it's an incomplete record, not corruption.
// This can happen e.g. on ext4 in the default data=ordered mode, when the
// filesize metadata is updated but the new data is not persisted.
// See https://news.ycombinator.com/item?id=11512006
if (IsAllZeros(length_and_cksum_buf)) {
bool all_zeros = false;
RETURN_NOT_OK(RestOfFileIsAllZeros(reader, file_size, tmp_offset, &all_zeros));
if (all_zeros) {
return Status::Incomplete("incomplete write of PB: rest of file is NULL bytes");
}
}
Slice length_checksum(length_and_cksum_buf.data() + sizeof(uint32_t), kPBContainerChecksumLen);
RETURN_NOT_OK_PREPEND(ParseAndCompareChecksum(length_checksum.data(), { length }),
CHECKSUM_ERR_MSG("Data length checksum does not match",
reader->filename(), tmp_offset - kPBContainerChecksumLen));
}
uint32_t data_length = DecodeFixed32(length.data());
// Read body and checksum into buffer for checksum & parsing.
uint64_t data_and_cksum_buflen = data_length + kPBContainerChecksumLen;
faststring body_and_cksum_buf;
RETURN_NOT_OK_PREPEND(ValidateAndReadData(reader, file_size, &tmp_offset, data_and_cksum_buflen,
&body_and_cksum_buf),
Substitute("Could not read PB message data from proto container file $0 "
"at offset $1",
reader->filename(), tmp_offset));
Slice body(body_and_cksum_buf.data(), data_length);
Slice record_checksum(body_and_cksum_buf.data() + data_length, kPBContainerChecksumLen);
// Version 1 has a single checksum for length, body.
// Version 2+ has individual checksums for length and body, respectively.
if (version == 1) {
RETURN_NOT_OK_PREPEND(ParseAndCompareChecksum(record_checksum.data(), { length, body }),
CHECKSUM_ERR_MSG("Length and data checksum does not match",
reader->filename(), tmp_offset - kPBContainerChecksumLen));
} else {
RETURN_NOT_OK_PREPEND(ParseAndCompareChecksum(record_checksum.data(), { body }),
CHECKSUM_ERR_MSG("Data checksum does not match",
reader->filename(), tmp_offset - kPBContainerChecksumLen));
}
// The checksum is correct. Time to decode the body.
//
// We could compare pb_type_ against msg.GetTypeName(), but:
// 1. pb_type_ is not available when reading the supplemental header,
// 2. ParseFromArray() should fail if the data cannot be parsed into the
// provided message type.
// To permit parsing of very large PB messages, we must use parse through a
// CodedInputStream and bump the byte limit. The SetTotalBytesLimit() docs
// say that 512MB is the shortest theoretical message length that may produce
// integer overflow warnings, so that's what we'll use.
ArrayInputStream ais(body.data(), body.size());
CodedInputStream cis(&ais);
cis.SetTotalBytesLimit(512 * 1024 * 1024);
if (PREDICT_FALSE(!msg->ParseFromCodedStream(&cis))) {
return Status::IOError("Unable to parse PB from path", reader->filename());
}
*offset = tmp_offset;
return Status::OK();
}
// Wrapper around ReadPBStartingAt() to enforce that we don't return
// Status::Incomplete() for V1 format files.
template<typename ReadableFileType>
Status ReadFullPB(ReadableFileType* reader, int version,
boost::optional<uint64_t>* cached_file_size,
uint64_t* offset, Message* msg) {
bool had_cached_size = *cached_file_size != boost::none;
Status s = ReadPBStartingAt(reader, version, cached_file_size, offset, msg);
if (PREDICT_FALSE(s.IsIncomplete() && version == 1)) {
return Status::Corruption("Unrecoverable incomplete record", s.ToString());
}
// If we hit EOF, but we were using a cached view of the file size, then it might be
// that the file has been extended. Invalidate the cache and try again.
if (had_cached_size && (s.IsIncomplete() || s.IsEndOfFile())) {
*cached_file_size = boost::none;
return ReadFullPB(reader, version, cached_file_size, offset, msg);
}
return s;
}
// Read and parse the protobuf container file-level header documented in pb_util.h.
template<typename ReadableFileType>
Status ParsePBFileHeader(ReadableFileType* reader, boost::optional<uint64_t>* cached_file_size,
uint64_t* offset, int* version) {
RETURN_NOT_OK(CacheFileSize(reader, cached_file_size));
uint64_t file_size = cached_file_size->get();
// We initially read enough data for a V2+ file header. This optimizes for
// V2+ and is valid on a V1 file because we don't consider these files valid
// unless they contain a record in addition to the file header. The
// additional 4 bytes required by a V2+ header (vs V1) is still less than the
// minimum number of bytes required for a V1 format data record.
uint64_t tmp_offset = *offset;
faststring header;
RETURN_NOT_OK_PREPEND(ValidateAndReadData(reader, file_size, &tmp_offset, kPBContainerV2HeaderLen,
&header),
Substitute("Could not read header for proto container file $0",
reader->filename()));
Slice magic_and_version(header.data(), kPBContainerMagicLen + sizeof(uint32_t));
Slice checksum(header.data() + kPBContainerMagicLen + sizeof(uint32_t), kPBContainerChecksumLen);
// Validate magic number.
if (PREDICT_FALSE(!strings::memeq(kPBContainerMagic, header.data(), kPBContainerMagicLen))) {
string file_magic(reinterpret_cast<const char*>(header.data()), kPBContainerMagicLen);
return Status::Corruption("Invalid magic number",
Substitute("Expected: $0, found: $1",
Utf8SafeCEscape(kPBContainerMagic),
Utf8SafeCEscape(file_magic)));
}
// Validate container file version.
uint32_t tmp_version = DecodeFixed32(header.data() + kPBContainerMagicLen);
if (PREDICT_FALSE(!IsSupportedContainerVersion(tmp_version))) {
return Status::NotSupported(
Substitute("Protobuf container has unsupported version: $0. Default version: $1",
tmp_version, kPBContainerDefaultVersion));
}
// Versions >= 2 have a checksum after the magic number and encoded version
// to ensure the integrity of these fields.
if (tmp_version >= 2) {
RETURN_NOT_OK_PREPEND(ParseAndCompareChecksum(checksum.data(), { magic_and_version }),
CHECKSUM_ERR_MSG("File header checksum does not match",
reader->filename(), tmp_offset - kPBContainerChecksumLen));
} else {
// Version 1 doesn't have a header checksum. Rewind our read offset so this
// data will be read again when we next attempt to read a data record.
tmp_offset -= kPBContainerChecksumLen;
}
*offset = tmp_offset;
*version = tmp_version;
return Status::OK();
}
// Read and parse the supplemental header from the container file.
template<typename ReadableFileType>
Status ReadSupplementalHeader(ReadableFileType* reader, int version,
boost::optional<uint64_t>* cached_file_size,
uint64_t* offset,
ContainerSupHeaderPB* sup_header) {
RETURN_NOT_OK_PREPEND(ReadFullPB(reader, version, cached_file_size, offset, sup_header),
Substitute("Could not read supplemental header from proto container file $0 "
"with version $1 at offset $2",
reader->filename(), version, *offset));
return Status::OK();
}
} // anonymous namespace
void AppendToString(const MessageLite &msg, faststring *output) {
DCHECK(msg.IsInitialized()) << InitializationErrorMessage("serialize", msg);
AppendPartialToString(msg, output);
}
void AppendPartialToString(const MessageLite &msg, faststring* output) {
size_t old_size = output->size();
size_t byte_size = msg.ByteSizeLong();
// Messages >2G cannot be serialized due to overflow computing ByteSize.
DCHECK_GE(byte_size, 0) << "Error computing ByteSize";
output->resize(old_size + byte_size);
uint8* start = &((*output)[old_size]);
uint8* end = msg.SerializeWithCachedSizesToArray(start);
if (end - start != byte_size) {
ByteSizeConsistencyError(byte_size, msg.ByteSizeLong(), end - start);
}
}
void SerializeToString(const MessageLite &msg, faststring *output) {
output->clear();
AppendToString(msg, output);
}
Status ParseFromSequentialFile(MessageLite *msg, SequentialFile *rfile) {
SequentialFileFileInputStream input(rfile);
if (!msg->ParseFromZeroCopyStream(&input)) {
RETURN_NOT_OK(input.status());
// If it's not a file IO error then it's a parsing error.
// Probably, we read wrong or damaged data here.
return Status::Corruption("Error parsing msg", InitializationErrorMessage("parse", *msg));
}
return Status::OK();
}
Status ParseFromArray(MessageLite* msg, const uint8_t* data, uint32_t length) {
if (!msg->ParseFromArray(data, length)) {
return Status::Corruption("Error parsing msg", InitializationErrorMessage("parse", *msg));
}
return Status::OK();
}
Status WritePBToPath(Env* env, const std::string& path,
const MessageLite& msg,
SyncMode sync) {
const string tmp_template = path + kTmpInfix + kTmpTemplateSuffix;
string tmp_path;
unique_ptr<WritableFile> file;
RETURN_NOT_OK(env->NewTempWritableFile(WritableFileOptions(), tmp_template, &tmp_path, &file));
auto tmp_deleter = MakeScopedCleanup([&]() {
WARN_NOT_OK(env->DeleteFile(tmp_path), "Could not delete file " + tmp_path);
});
WritableFileOutputStream output(file.get());
bool res = msg.SerializeToZeroCopyStream(&output);
if (!res || !output.Flush()) {
return Status::IOError("Unable to serialize PB to file");
}
if (sync == pb_util::SYNC) {
RETURN_NOT_OK_PREPEND(file->Sync(), "Failed to Sync() " + tmp_path);
}
RETURN_NOT_OK_PREPEND(file->Close(), "Failed to Close() " + tmp_path);
RETURN_NOT_OK_PREPEND(env->RenameFile(tmp_path, path), "Failed to rename tmp file to " + path);
tmp_deleter.cancel();
if (sync == pb_util::SYNC) {
RETURN_NOT_OK_PREPEND(env->SyncDir(DirName(path)), "Failed to SyncDir() parent of " + path);
}
return Status::OK();
}
Status ReadPBFromPath(Env* env, const std::string& path, MessageLite* msg) {
shared_ptr<SequentialFile> rfile;
RETURN_NOT_OK(env_util::OpenFileForSequential(env, path, &rfile));
RETURN_NOT_OK(ParseFromSequentialFile(msg, rfile.get()));
return Status::OK();
}
static void TruncateString(string* s, int max_len) {
if (s->size() > max_len) {
s->resize(max_len);
s->append("<truncated>");
}
}
void TruncateFields(Message* message, int max_len) {
const Reflection* reflection = message->GetReflection();
vector<const FieldDescriptor*> fields;
reflection->ListFields(*message, &fields);
for (const FieldDescriptor* field : fields) {
if (field->is_repeated()) {
for (int i = 0; i < reflection->FieldSize(*message, field); i++) {
switch (field->cpp_type()) {
case FieldDescriptor::CPPTYPE_STRING: {
const string& s_const = reflection->GetRepeatedStringReference(*message, field, i,
nullptr);
TruncateString(const_cast<string*>(&s_const), max_len);
break;
}
case FieldDescriptor::CPPTYPE_MESSAGE: {
TruncateFields(reflection->MutableRepeatedMessage(message, field, i), max_len);
break;
}
default:
break;
}
}
} else {
switch (field->cpp_type()) {
case FieldDescriptor::CPPTYPE_STRING: {
const string& s_const = reflection->GetStringReference(*message, field, nullptr);
TruncateString(const_cast<string*>(&s_const), max_len);
break;
}
case FieldDescriptor::CPPTYPE_MESSAGE: {
TruncateFields(reflection->MutableMessage(message, field), max_len);
break;
}
default:
break;
}
}
}
}
namespace {
class SecureFieldPrinter : public TextFormat::FastFieldValuePrinter {
public:
using super = TextFormat::FastFieldValuePrinter;
using BaseTextGenerator = TextFormat::BaseTextGenerator;
void PrintFieldName(const Message& message,
const Reflection* reflection,
const FieldDescriptor* field,
BaseTextGenerator* generator) const override {
hide_next_string_ = field->cpp_type() == FieldDescriptor::CPPTYPE_STRING &&
field->options().GetExtension(REDACT);
super::PrintFieldName(message, reflection, field, generator);
}
void PrintFieldName(const Message& message, int field_index,
int field_count, const Reflection* reflection,
const FieldDescriptor* field,
BaseTextGenerator* generator) const override {
hide_next_string_ = field->cpp_type() == FieldDescriptor::CPPTYPE_STRING &&
field->options().GetExtension(REDACT);
super::PrintFieldName(message, field_index, field_count, reflection, field,
generator);
}
void PrintString(const string& val, BaseTextGenerator* generator) const override {
if (hide_next_string_) {
hide_next_string_ = false;
super::PrintString(KUDU_REDACT(val), generator);
return;
}
super::PrintString(val, generator);
}
void PrintBytes(const string& val, BaseTextGenerator* generator) const override {
if (hide_next_string_) {
hide_next_string_ = false;
super::PrintBytes(KUDU_REDACT(val), generator);
}
super::PrintBytes(val, generator);
}
mutable bool hide_next_string_ = false;
};
} // anonymous namespace
string SecureDebugString(const Message& msg) {
string debug_string;
TextFormat::Printer printer;
printer.SetDefaultFieldValuePrinter(new SecureFieldPrinter());
printer.PrintToString(msg, &debug_string);
return debug_string;
}
string SecureShortDebugString(const Message& msg) {
string debug_string;
TextFormat::Printer printer;
printer.SetSingleLineMode(true);
printer.SetDefaultFieldValuePrinter(new SecureFieldPrinter());
printer.PrintToString(msg, &debug_string);
// Single line mode currently might have an extra space at the end.
if (!debug_string.empty() &&
debug_string[debug_string.size() - 1] == ' ') {
debug_string.resize(debug_string.size() - 1);
}
return debug_string;
}
WritablePBContainerFile::WritablePBContainerFile(shared_ptr<RWFile> writer)
: state_(FileState::NOT_INITIALIZED),
offset_(0),
version_(kPBContainerDefaultVersion),
writer_(std::move(writer)) {
}
WritablePBContainerFile::~WritablePBContainerFile() {
WARN_NOT_OK(Close(), "Could not Close() when destroying file");
}
Status WritablePBContainerFile::SetVersionForTests(int version) {
DCHECK_EQ(FileState::NOT_INITIALIZED, state_);
if (!IsSupportedContainerVersion(version)) {
return Status::NotSupported(Substitute("Version $0 is not supported", version));
}
version_ = version;
return Status::OK();
}
Status WritablePBContainerFile::CreateNew(const Message& msg) {
DCHECK_EQ(FileState::NOT_INITIALIZED, state_);
const uint64_t kHeaderLen = (version_ == 1) ? kPBContainerV1HeaderLen
: kPBContainerV1HeaderLen + kPBContainerChecksumLen;
faststring buf;
buf.resize(kHeaderLen);
// Serialize the magic.
strings::memcpy_inlined(buf.data(), kPBContainerMagic, kPBContainerMagicLen);
uint64_t offset = kPBContainerMagicLen;
// Serialize the version.
InlineEncodeFixed32(buf.data() + offset, version_);
offset += sizeof(uint32_t);
DCHECK_EQ(kPBContainerV1HeaderLen, offset)
<< "Serialized unexpected number of total bytes";
// Versions >= 2: Checksum the magic and version.
if (version_ >= 2) {
uint32_t header_checksum = crc::Crc32c(buf.data(), offset);
InlineEncodeFixed32(buf.data() + offset, header_checksum);
offset += sizeof(uint32_t);
}
DCHECK_EQ(offset, kHeaderLen);
// Serialize the supplemental header.
ContainerSupHeaderPB sup_header;
PopulateDescriptorSet(msg.GetDescriptor()->file(),
sup_header.mutable_protos());
sup_header.set_pb_type(msg.GetTypeName());
RETURN_NOT_OK_PREPEND(AppendMsgToBuffer(sup_header, &buf),
"Failed to prepare supplemental header for writing");
// Write the serialized buffer to the file.
RETURN_NOT_OK_PREPEND(AppendBytes(buf),
"Failed to append header to file");
state_ = FileState::OPEN;
return Status::OK();
}
Status WritablePBContainerFile::OpenExisting() {
DCHECK_EQ(FileState::NOT_INITIALIZED, state_);
boost::optional<uint64_t> size;
RETURN_NOT_OK(ParsePBFileHeader(writer_.get(), &size, &offset_, &version_));
ContainerSupHeaderPB sup_header;
RETURN_NOT_OK(ReadSupplementalHeader(writer_.get(), version_, &size,
&offset_, &sup_header));
offset_ = size.get(); // Reset the write offset to the end of the file.
state_ = FileState::OPEN;
return Status::OK();
}
Status WritablePBContainerFile::AppendBytes(const Slice& data) {
std::lock_guard<Mutex> l(offset_lock_);
RETURN_NOT_OK(writer_->Write(offset_, data));
offset_ += data.size();
return Status::OK();
}
Status WritablePBContainerFile::Append(const Message& msg) {
DCHECK_EQ(FileState::OPEN, state_);
faststring buf;
RETURN_NOT_OK_PREPEND(AppendMsgToBuffer(msg, &buf),
"Failed to prepare buffer for writing");
RETURN_NOT_OK_PREPEND(AppendBytes(buf), "Failed to append data to file");
return Status::OK();
}
Status WritablePBContainerFile::Flush() {
DCHECK_EQ(FileState::OPEN, state_);
// TODO: Flush just the dirty bytes.
RETURN_NOT_OK_PREPEND(writer_->Flush(RWFile::FLUSH_ASYNC, 0, 0), "Failed to Flush() file");
return Status::OK();
}
Status WritablePBContainerFile::Sync() {
DCHECK_EQ(FileState::OPEN, state_);
RETURN_NOT_OK_PREPEND(writer_->Sync(), "Failed to Sync() file");
return Status::OK();
}
Status WritablePBContainerFile::Close() {
if (state_ != FileState::CLOSED) {
state_ = FileState::CLOSED;
Status s = writer_->Close();
writer_.reset();
RETURN_NOT_OK_PREPEND(s, "Failed to Close() file");
}
return Status::OK();
}
const string& WritablePBContainerFile::filename() const {
return writer_->filename();
}
Status WritablePBContainerFile::AppendMsgToBuffer(const Message& msg, faststring* buf) {
DCHECK(msg.IsInitialized()) << InitializationErrorMessage("serialize", msg);
size_t data_len_long = msg.ByteSizeLong();
// Messages >2G cannot be serialized due to format restrictions.
CHECK_LT(data_len_long, std::numeric_limits<int32_t>::max());
uint32_t data_len = static_cast<uint32_t>(data_len_long);
uint64_t record_buflen = sizeof(uint32_t) + data_len + sizeof(uint32_t);
if (version_ >= 2) {
record_buflen += sizeof(uint32_t); // Additional checksum just for the length.
}
// Grow the buffer to hold the new data.
uint64_t record_offset = buf->size();
buf->resize(record_offset + record_buflen);
uint8_t* dst = buf->data() + record_offset;
// Serialize the data length.
size_t cur_offset = 0;
InlineEncodeFixed32(dst + cur_offset, data_len);
cur_offset += sizeof(uint32_t);
// For version >= 2: Serialize the checksum of the data length.
if (version_ >= 2) {
uint32_t length_checksum = crc::Crc32c(&data_len, sizeof(data_len));
InlineEncodeFixed32(dst + cur_offset, length_checksum);
cur_offset += sizeof(uint32_t);
}
// Serialize the data.
uint64_t data_offset = cur_offset;
if (PREDICT_FALSE(!msg.SerializeWithCachedSizesToArray(dst + cur_offset))) {
return Status::IOError("Failed to serialize PB to array");
}
cur_offset += data_len;
// Calculate and serialize the data checksum.
// For version 1, this is the checksum of the len + data.
// For version >= 2, this is only the checksum of the data.
uint32_t data_checksum;
if (version_ == 1) {
data_checksum = crc::Crc32c(dst, cur_offset);
} else {
data_checksum = crc::Crc32c(dst + data_offset, data_len);
}
InlineEncodeFixed32(dst + cur_offset, data_checksum);
cur_offset += sizeof(uint32_t);
DCHECK_EQ(record_buflen, cur_offset) << "Serialized unexpected number of total bytes";
return Status::OK();
}
void WritablePBContainerFile::PopulateDescriptorSet(
const FileDescriptor* desc, FileDescriptorSet* output) {
// Because we don't compile protobuf with TSAN enabled, copying the
// static PB descriptors in this function ends up triggering a lot of
// race reports. We suppress the reports, but TSAN still has to walk
// the stack, etc, and this function becomes very slow. So, we ignore
// TSAN here.
debug::ScopedTSANIgnoreReadsAndWrites ignore_tsan;
FileDescriptorSet all_descs;
// Tracks all schemas that have been added to 'unemitted' at one point
// or another. Is a superset of 'unemitted' and only ever grows.
unordered_set<const FileDescriptor*> processed;
// Tracks all remaining unemitted schemas.
deque<const FileDescriptor*> unemitted;
InsertOrDie(&processed, desc);
unemitted.push_front(desc);
while (!unemitted.empty()) {
const FileDescriptor* proto = unemitted.front();
// The current schema is emitted iff we've processed (i.e. emitted) all
// of its dependencies.
bool emit = true;
for (int i = 0; i < proto->dependency_count(); i++) {
const FileDescriptor* dep = proto->dependency(i);
if (InsertIfNotPresent(&processed, dep)) {
unemitted.push_front(dep);
emit = false;
}
}
if (emit) {
unemitted.pop_front();
proto->CopyTo(all_descs.mutable_file()->Add());
}
}
all_descs.Swap(output);
}
ReadablePBContainerFile::ReadablePBContainerFile(shared_ptr<RandomAccessFile> reader)
: state_(FileState::NOT_INITIALIZED),
version_(kPBContainerInvalidVersion),
offset_(0),
reader_(std::move(reader)) {
}
ReadablePBContainerFile::~ReadablePBContainerFile() {
Close();
}
Status ReadablePBContainerFile::Open() {
DCHECK_EQ(FileState::NOT_INITIALIZED, state_);
RETURN_NOT_OK(ParsePBFileHeader(reader_.get(), &cached_file_size_, &offset_, &version_));
ContainerSupHeaderPB sup_header;
RETURN_NOT_OK(ReadSupplementalHeader(reader_.get(), version_, &cached_file_size_,
&offset_, &sup_header));
protos_.reset(sup_header.release_protos());
pb_type_ = sup_header.pb_type();
state_ = FileState::OPEN;
return Status::OK();
}
Status ReadablePBContainerFile::ReadNextPB(Message* msg) {
DCHECK_EQ(FileState::OPEN, state_);
return ReadFullPB(reader_.get(), version_, &cached_file_size_, &offset_, msg);
}
Status ReadablePBContainerFile::GetPrototype(const Message** prototype) {
if (!prototype_) {
// Loading the schemas into a DescriptorDatabase (and not directly into
// a DescriptorPool) defers resolution until FindMessageTypeByName()
// below, allowing for schemas to be loaded in any order.
unique_ptr<SimpleDescriptorDatabase> db(new SimpleDescriptorDatabase());
for (int i = 0; i < protos()->file_size(); i++) {
if (!db->Add(protos()->file(i))) {
return Status::Corruption("Descriptor not loaded", Substitute(
"Could not load descriptor for PB type $0 referenced in container file",
pb_type()));
}
}
unique_ptr<DescriptorPool> pool(new DescriptorPool(db.get()));
const Descriptor* desc = pool->FindMessageTypeByName(pb_type());
if (!desc) {
return Status::NotFound("Descriptor not found", Substitute(
"Could not find descriptor for PB type $0 referenced in container file",
pb_type()));
}
unique_ptr<DynamicMessageFactory> factory(new DynamicMessageFactory());
const Message* p = factory->GetPrototype(desc);
if (!p) {
return Status::NotSupported("Descriptor not supported", Substitute(
"Descriptor $0 referenced in container file not supported",
pb_type()));
}
db_ = std::move(db);
descriptor_pool_ = std::move(pool);
message_factory_ = std::move(factory);
prototype_ = p;
}
*prototype = prototype_;
return Status::OK();
}
Status ReadablePBContainerFile::Dump(ostream* os, ReadablePBContainerFile::Format format) {
DCHECK_EQ(FileState::OPEN, state_);
// Since we use the protobuf library support for dumping JSON, there isn't any easy
// way to hook in our redaction support. Since this is only used by CLI tools,
// just refuse to dump JSON if redaction is enabled.
if (format == Format::JSON && KUDU_SHOULD_REDACT()) {
return Status::NotSupported("cannot dump PBC file in JSON format if redaction is enabled");
}
const char* const kDashes = "-------";
if (format == Format::DEBUG) {
*os << "File header" << endl;
*os << kDashes << endl;
*os << "Protobuf container version: " << version_ << endl;
*os << "Total container file size: " << *cached_file_size_ << endl;
*os << "Entry PB type: " << pb_type_ << endl;
*os << endl;
}
// Use the embedded protobuf information from the container file to
// create the appropriate kind of protobuf Message.
const Message* prototype;
RETURN_NOT_OK(GetPrototype(&prototype));
unique_ptr<Message> msg(prototype_->New());
// Dump each message in the container file.
int count = 0;
uint64_t prev_offset = offset_;
Status s;
string buf;
for (s = ReadNextPB(msg.get());
s.ok();
s = ReadNextPB(msg.get())) {
switch (format) {
case Format::ONELINE:
*os << count << "\t" << SecureShortDebugString(*msg) << endl;
break;
case Format::DEFAULT:
case Format::DEBUG:
*os << "Message " << count << endl;
if (format == Format::DEBUG) {
*os << "offset: " << prev_offset << endl;
*os << "length: " << (offset_ - prev_offset) << endl;
}
*os << kDashes << endl;
*os << SecureDebugString(*msg) << endl;
break;
case Format::JSON:
buf.clear();
const auto& google_status = google::protobuf::util::MessageToJsonString(
*msg, &buf, google::protobuf::util::JsonPrintOptions());
if (!google_status.ok()) {
return Status::RuntimeError("could not convert PB to JSON", google_status.ToString());
}
*os << buf << endl;
break;
}
prev_offset = offset_;
count++;
}
if (format == Format::DEBUG && !s.IsEndOfFile()) {
*os << "Message " << count << endl;
*os << "error: failed to parse protobuf message" << endl;
*os << "offset: " << prev_offset << endl;
*os << "remaining file length: " << (*cached_file_size_ - prev_offset) << endl;
*os << kDashes << endl;
}
return s.IsEndOfFile() ? Status::OK() : s;
}
Status ReadablePBContainerFile::Close() {
state_ = FileState::CLOSED;
reader_.reset();
return Status::OK();
}
int ReadablePBContainerFile::version() const {
DCHECK_EQ(FileState::OPEN, state_);
return version_;
}
uint64_t ReadablePBContainerFile::offset() const {
DCHECK_EQ(FileState::OPEN, state_);
return offset_;
}
Status ReadPBContainerFromPath(Env* env, const std::string& path, Message* msg) {
unique_ptr<RandomAccessFile> file;
RETURN_NOT_OK(env->NewRandomAccessFile(path, &file));
ReadablePBContainerFile pb_file(std::move(file));
RETURN_NOT_OK(pb_file.Open());
RETURN_NOT_OK(pb_file.ReadNextPB(msg));
return pb_file.Close();
}
Status WritePBContainerToPath(Env* env, const std::string& path,
const Message& msg,
CreateMode create,
SyncMode sync) {
TRACE_EVENT2("io", "WritePBContainerToPath",
"path", path,
"msg_type", msg.GetTypeName());
if (create == NO_OVERWRITE && env->FileExists(path)) {
return Status::AlreadyPresent(Substitute("File $0 already exists", path));
}
const string tmp_template = path + kTmpInfix + kTmpTemplateSuffix;
string tmp_path;
unique_ptr<RWFile> file;
RETURN_NOT_OK(env->NewTempRWFile(RWFileOptions(), tmp_template, &tmp_path, &file));
auto tmp_deleter = MakeScopedCleanup([&]() {
WARN_NOT_OK(env->DeleteFile(tmp_path), "Could not delete file " + tmp_path);
});
WritablePBContainerFile pb_file(std::move(file));
RETURN_NOT_OK(pb_file.CreateNew(msg));
RETURN_NOT_OK(pb_file.Append(msg));
if (sync == pb_util::SYNC) {
RETURN_NOT_OK(pb_file.Sync());
}
RETURN_NOT_OK(pb_file.Close());
RETURN_NOT_OK_PREPEND(env->RenameFile(tmp_path, path),
"Failed to rename tmp file to " + path);
tmp_deleter.cancel();
if (sync == pb_util::SYNC) {
RETURN_NOT_OK_PREPEND(env->SyncDir(DirName(path)),
"Failed to SyncDir() parent of " + path);
}
return Status::OK();
}
scoped_refptr<debug::ConvertableToTraceFormat> PbTracer::TracePb(const Message& msg) {
return make_scoped_refptr(new PbTracer(msg));
}
PbTracer::PbTracer(const Message& msg) : msg_(msg.New()) {
msg_->CopyFrom(msg);
}
void PbTracer::AppendAsTraceFormat(std::string* out) const {
pb_util::TruncateFields(msg_.get(), kMaxFieldLengthToTrace);
std::ostringstream ss;
JsonWriter jw(&ss, JsonWriter::COMPACT);
jw.Protobuf(*msg_);
out->append(ss.str());
}
} // namespace pb_util
} // namespace kudu