blob: 525ec21e368b78e53a19801a646f09c2bd554ea5 [file] [log] [blame]
#include "binarystore.hpp"
#include "sys_file.hpp"
#include <pb_decode.h>
#include <pb_encode.h>
#include <sys/types.h>
#include <unistd.h>
#include <algorithm>
#include <blobs-ipmid/blobs.hpp>
#include <boost/endian/arithmetic.hpp>
#include <cstdint>
#include <ipmid/handler.hpp>
#include <map>
#include <memory>
#include <optional>
#include <phosphor-logging/elog.hpp>
#include <stdplus/str/cat.hpp>
#include <string>
#include <vector>
#include "binaryblob.pb.n.h"
using std::size_t;
using std::uint16_t;
using std::uint32_t;
using std::uint64_t;
using std::uint8_t;
namespace binstore
{
using namespace phosphor::logging;
std::unique_ptr<BinaryStoreInterface> BinaryStore::createFromConfig(
const std::string& baseBlobId, std::unique_ptr<SysFile> file,
std::optional<uint32_t> maxSize, std::optional<std::string> aliasBlobBaseId)
{
if (baseBlobId.empty() || !file)
{
log<level::ERR>("Unable to create binarystore from invalid config",
entry("BASE_ID=%s", baseBlobId.c_str()));
return nullptr;
}
auto store =
std::make_unique<BinaryStore>(baseBlobId, std::move(file), maxSize);
if (!store->loadSerializedData(aliasBlobBaseId))
{
return nullptr;
}
return store;
}
std::unique_ptr<BinaryStoreInterface>
BinaryStore::createFromFile(std::unique_ptr<SysFile> file, bool readOnly,
std::optional<uint32_t> maxSize)
{
if (!file)
{
log<level::ERR>("Unable to create binarystore from invalid file");
return nullptr;
}
auto store =
std::make_unique<BinaryStore>(std::move(file), readOnly, maxSize);
if (!store->loadSerializedData())
{
return nullptr;
}
return store;
}
template <typename S>
static constexpr auto pbDecodeStr = [](pb_istream_t* stream,
const pb_field_iter_t*,
void** arg) noexcept {
static_assert(sizeof(*std::declval<S>().data()) == sizeof(pb_byte_t));
auto& s = *reinterpret_cast<S*>(*arg);
s.resize(stream->bytes_left);
return pb_read(stream, reinterpret_cast<pb_byte_t*>(s.data()), s.size());
};
template <typename T>
static pb_callback_t pbStrDecoder(T& t) noexcept
{
return {{.decode = pbDecodeStr<T>}, &t};
}
template <typename S>
static constexpr auto pbEncodeStr = [](pb_ostream_t* stream,
const pb_field_iter_t* field,
void* const* arg) noexcept {
static_assert(sizeof(*std::declval<S>().data()) == sizeof(pb_byte_t));
const auto& s = *reinterpret_cast<const S*>(*arg);
return pb_encode_tag_for_field(stream, field) &&
pb_encode_string(
stream, reinterpret_cast<const pb_byte_t*>(s.data()), s.size());
};
template <typename T>
static pb_callback_t pbStrEncoder(const T& t) noexcept
{
return {{.encode = pbEncodeStr<T>}, const_cast<T*>(&t)};
}
bool BinaryStore::loadSerializedData(std::optional<std::string> aliasBlobBaseId)
{
/* Load blob from sysfile if we know it might not match what we have.
* Note it will overwrite existing unsaved data per design. */
if (commitState_ == CommitState::Clean ||
commitState_ == CommitState::Uninitialized)
{
return true;
}
log<level::NOTICE>("Try loading blob from persistent data",
entry("BASE_ID=%s", baseBlobId_.c_str()));
std::string protoBlobId;
static constexpr auto blobcb = [](pb_istream_t* stream,
const pb_field_iter_t*,
void** arg) noexcept {
std::string id;
std::vector<std::uint8_t> data;
binstore_binaryblobproto_BinaryBlob msg = {
.blob_id = pbStrDecoder(id),
.data = pbStrDecoder(data),
};
if (!pb_decode(stream, binstore_binaryblobproto_BinaryBlob_fields,
&msg))
{
return false;
}
reinterpret_cast<decltype(std::declval<BinaryStore>().blobs_)*>(*arg)
->emplace(id, data);
return true;
};
try
{
/* Parse length-prefixed format to protobuf */
boost::endian::little_uint64_t size = 0;
file_->readToBuf(0, sizeof(size), reinterpret_cast<char*>(&size));
auto proto = file_->readAsStr(sizeof(size), size);
auto ist = pb_istream_from_buffer(
reinterpret_cast<const pb_byte_t*>(proto.data()), proto.size());
binstore_binaryblobproto_BinaryBlobBase msg = {
.blob_base_id = pbStrDecoder(protoBlobId),
.blobs = {{.decode = blobcb}, &blobs_},
};
blobs_.clear(); // Purge old contents before new append during decode
if (!pb_decode(&ist, binstore_binaryblobproto_BinaryBlobBase_fields,
&msg))
{
/* Fail to parse the data, which might mean no preexsiting blobs
* and is a valid case to handle. Simply init an empty binstore. */
commitState_ = CommitState::Uninitialized;
}
}
catch (const std::system_error& e)
{
/* Read causes unexpected system-level failure */
log<level::ERR>("Reading from sysfile failed",
entry("ERROR=%s", e.what()));
return false;
}
catch (const std::exception& e)
{
/* Non system error originates from junk value in 'size' */
commitState_ = CommitState::Uninitialized;
}
if (commitState_ == CommitState::Uninitialized)
{
log<level::WARNING>("Fail to parse. There might be no persisted blobs",
entry("BASE_ID=%s", baseBlobId_.c_str()));
return true;
}
if (baseBlobId_.empty() && !protoBlobId.empty())
{
baseBlobId_ = std::move(protoBlobId);
}
else if (protoBlobId == aliasBlobBaseId)
{
log<level::WARNING>("Alias blob id, rename blob id...",
entry("LOADED=%s", protoBlobId.c_str()),
entry("RENAMED=%s", baseBlobId_.c_str()));
std::string tmpBlobId = baseBlobId_;
baseBlobId_ = *aliasBlobBaseId;
return setBaseBlobId(tmpBlobId);
}
else if (protoBlobId != baseBlobId_ && !readOnly_)
{
/* Uh oh, stale data loaded. Clean it and commit. */
// TODO: it might be safer to add an option in config to error out
// instead of to overwrite.
log<level::ERR>("Stale blob data, resetting internals...",
entry("LOADED=%s", protoBlobId.c_str()),
entry("EXPECTED=%s", baseBlobId_.c_str()));
blobs_.clear();
return this->commit();
}
return true;
}
std::string BinaryStore::getBaseBlobId() const
{
return baseBlobId_;
}
bool BinaryStore::setBaseBlobId(const std::string& baseBlobId)
{
for (auto it = blobs_.begin(); it != blobs_.end();)
{
auto curr = it++;
if (curr->first.starts_with(baseBlobId_))
{
auto nh = blobs_.extract(curr);
nh.key() = stdplus::strCat(
baseBlobId,
std::string_view(curr->first).substr(baseBlobId_.size()));
blobs_.insert(std::move(nh));
}
}
baseBlobId_ = baseBlobId;
return this->commit();
}
std::vector<std::string> BinaryStore::getBlobIds() const
{
std::vector<std::string> result;
result.reserve(blobs_.size() + 1);
result.emplace_back(getBaseBlobId());
for (const auto& kv : blobs_)
{
result.emplace_back(kv.first);
}
return result;
}
bool BinaryStore::openOrCreateBlob(const std::string& blobId, uint16_t flags)
{
if (!(flags & blobs::OpenFlags::read))
{
log<level::ERR>("OpenFlags::read not specified when opening",
entry("BLOB_ID=%s", blobId.c_str()));
return false;
}
if (!currentBlob_.empty())
{
log<level::ERR>("Already handling a different blob",
entry("EXPECTED=%s", currentBlob_.c_str()),
entry("RECEIVED=%s", blobId.c_str()));
return false;
}
if (readOnly_ && (flags & blobs::OpenFlags::write))
{
log<level::ERR>("Can't open the blob for writing: read-only store",
entry("BLOB_ID=%s", blobId.c_str()));
return false;
}
writable_ = flags & blobs::OpenFlags::write;
/* If there are uncommitted data, discard them. */
if (!this->loadSerializedData())
{
return false;
}
/* Iterate and find if there is an existing blob with this id.
* blobsPtr points to a BinaryBlob container with STL-like semantics*/
if (blobs_.find(blobId) != blobs_.end())
{
currentBlob_ = blobId;
return true;
}
/* Otherwise, create the blob and append it */
if (readOnly_)
{
return false;
}
blobs_.emplace(blobId, std::vector<std::uint8_t>{});
currentBlob_ = blobId;
commitState_ = CommitState::Dirty;
log<level::NOTICE>("Created new blob", entry("BLOB_ID=%s", blobId.c_str()));
return true;
}
bool BinaryStore::deleteBlob(const std::string&)
{
return false;
}
std::vector<uint8_t> BinaryStore::read(uint32_t offset, uint32_t requestedSize)
{
if (currentBlob_.empty())
{
log<level::ERR>("No open blob to read");
return {};
}
const auto& data = blobs_.find(currentBlob_)->second;
/* If it is out of bound, return empty vector */
if (offset >= data.size())
{
log<level::ERR>("Read offset is beyond data size",
entry("MAX_SIZE=0x%x", data.size()),
entry("RECEIVED_OFFSET=0x%x", offset));
return {};
}
auto s = data.begin() + offset;
return {s, s + std::min<size_t>(requestedSize, data.size() - offset)};
}
std::vector<uint8_t> BinaryStore::readBlob(const std::string& blobId) const
{
const auto blobIt = blobs_.find(blobId);
if (blobIt == blobs_.end())
{
throw ipmi::HandlerCompletion(ipmi::ccUnspecifiedError);
}
return blobIt->second;
}
static binstore_binaryblobproto_BinaryBlobBase makeEncoder(
const std::string& base,
const std::map<std::string, std::vector<std::uint8_t>>& blobs) noexcept
{
using Blobs = std::decay_t<decltype(blobs)>;
static constexpr auto blobcb = [](pb_ostream_t* stream,
const pb_field_iter_t* field,
void* const* arg) noexcept {
const auto& blobs = *reinterpret_cast<const Blobs*>(*arg);
for (const auto& [id, data] : blobs)
{
binstore_binaryblobproto_BinaryBlob msg = {
.blob_id = pbStrEncoder(id),
.data = pbStrEncoder(data),
};
if (!pb_encode_tag_for_field(stream, field) ||
!pb_encode_submessage(
stream, binstore_binaryblobproto_BinaryBlob_fields, &msg))
{
return false;
}
}
return true;
};
return {
.blob_base_id = pbStrEncoder(base),
.blobs = {{.encode = blobcb},
const_cast<void*>(reinterpret_cast<const void*>(&blobs))},
};
}
static std::size_t
payloadCalcSize(const binstore_binaryblobproto_BinaryBlobBase& msg)
{
pb_ostream_t nost = {};
if (!pb_encode(&nost, binstore_binaryblobproto_BinaryBlobBase_fields, &msg))
{
throw std::runtime_error(
std::format("Calculating msg size: {}", PB_GET_ERROR(&nost)));
}
// Proto is prepended with the size of the proto
return nost.bytes_written + sizeof(boost::endian::little_uint64_t);
}
bool BinaryStore::write(uint32_t offset, const std::vector<uint8_t>& data)
{
if (currentBlob_.empty())
{
log<level::ERR>("No open blob to write");
return false;
}
if (!writable_)
{
log<level::ERR>("Open blob is not writable");
return false;
}
auto& bdata = blobs_.find(currentBlob_)->second;
if (offset > bdata.size())
{
log<level::ERR>("Write would leave a gap with undefined data. Return.");
return false;
}
std::size_t oldsize = bdata.size(), reqSize = offset + data.size();
if (reqSize > bdata.size())
{
bdata.resize(reqSize);
}
if (payloadCalcSize(makeEncoder(baseBlobId_, blobs_)) >
maxSize.value_or(
std::numeric_limits<std::decay_t<decltype(*maxSize)>>::max()))
{
log<level::ERR>("Write data would make the total size exceed the max "
"size allowed. Return.");
bdata.resize(oldsize);
return false;
}
commitState_ = CommitState::Dirty;
std::copy(data.begin(), data.end(), bdata.data() + offset);
return true;
}
bool BinaryStore::commit()
{
if (readOnly_)
{
log<level::ERR>("ReadOnly blob, not committing");
return false;
}
/* Store as little endian to be platform agnostic. Consistent with read. */
auto msg = makeEncoder(baseBlobId_, blobs_);
auto outSize = payloadCalcSize(msg);
if (outSize >
maxSize.value_or(
std::numeric_limits<std::decay_t<decltype(*maxSize)>>::max()))
{
log<level::ERR>("Commit Data exceeded maximum allowed size");
return false;
}
std::string buf(outSize, '\0');
auto& size = *reinterpret_cast<boost::endian::little_uint64_t*>(buf.data());
auto ost = pb_ostream_from_buffer(reinterpret_cast<pb_byte_t*>(buf.data()) +
sizeof(size),
buf.size() - sizeof(size));
pb_encode(&ost, binstore_binaryblobproto_BinaryBlobBase_fields, &msg);
size = ost.bytes_written;
try
{
file_->writeStr(buf, 0);
}
catch (const std::exception& e)
{
commitState_ = CommitState::CommitError;
log<level::ERR>("Writing to sysfile failed",
entry("ERROR=%s", e.what()));
return false;
};
commitState_ = CommitState::Clean;
return true;
}
bool BinaryStore::close()
{
currentBlob_.clear();
writable_ = false;
commitState_ = CommitState::Dirty;
return true;
}
/*
* Sets |meta| with size and state of the blob. Returns |blobState| with
* standard definition from phosphor-ipmi-blobs header blob.hpp, plus OEM
* flag bits BinaryStore::CommitState.
enum StateFlags
{
open_read = (1 << 0),
open_write = (1 << 1),
committing = (1 << 2),
committed = (1 << 3),
commit_error = (1 << 4),
};
enum CommitState
{
Dirty = (1 << 8), // In-memory data might not match persisted data
Clean = (1 << 9), // In-memory data matches persisted data
Uninitialized = (1 << 10), // Cannot find persisted data
CommitError = (1 << 11) // Error happened during committing
};
*/
bool BinaryStore::stat(blobs::BlobMeta* meta)
{
uint16_t blobState = blobs::StateFlags::open_read;
if (writable_)
{
blobState |= blobs::StateFlags::open_write;
}
if (commitState_ == CommitState::Clean)
{
blobState |= blobs::StateFlags::committed;
}
else if (commitState_ == CommitState::CommitError)
{
blobState |= blobs::StateFlags::commit_error;
}
blobState |= commitState_;
if (!currentBlob_.empty())
{
meta->size = blobs_.find(currentBlob_)->second.size();
}
else
{
meta->size = 0;
}
meta->blobState = blobState;
return true;
}
} // namespace binstore