| #pragma once |
| |
| #include "cper.hpp" |
| |
| #include <chrono> |
| #include <concepts> |
| #include <cstdint> |
| #include <cstring> |
| #include <string> |
| |
| namespace uefi::cper |
| { |
| template <typename T> |
| concept ByteLike = std::is_trivially_copyable_v<T> && sizeof(T) == 1; |
| |
| /** |
| * @brief Helper function to convert time values into BCD format. |
| * |
| * @example You may have minutes represented as base10 20 which is 0x14. BCD |
| * expects the values to be represented as 0x20. |
| * |
| * @param[in] val - The value to convert to BCD. |
| * |
| * @returns a byte with the BCD val. |
| */ |
| uint8_t convertToBcd(uint8_t val) |
| { |
| return (0x10 * (val / 10)) + (val % 10); |
| } |
| |
| /** |
| * @brief Helper function to create a UTC UEFI CPER timestamp from a |
| * std::chrono time point. |
| * https://uefi.org/specs/UEFI/2.10/Apx_N_Common_Platform_Error_Record.html#record-header |
| * |
| * @param[in] timeOfCollection - A std::chrono time_point representing the time |
| * the fault was detected by the system software. |
| * @param[in] isPrecise - Bool which indicates if the time is precise. |
| * |
| * @param[out] A uefi cper timestamp as uint64_t. |
| */ |
| uint64_t createCperTimestamp( |
| const std::chrono::time_point<std::chrono::system_clock> timeOfCollection, |
| const bool isPrecise = true) |
| { |
| // Number of years in a century. |
| constexpr int kCentury = 100; |
| |
| // The tm_year counts the number of years since 1900. |
| // https://en.cppreference.com/w/c/chrono/tm |
| constexpr int kTmYearOffset = 1900; |
| |
| // UEFI expects the isPrecise byte bits [1:7] to be zero. |
| const uint8_t isPreciseBit = 0 | (isPrecise & 0x1); |
| |
| const time_t tt = std::chrono::system_clock::to_time_t(timeOfCollection); |
| const tm utcTime = *gmtime(&tt); |
| |
| // The uefi timestamp breaks the calendar year into two bytes |
| // representing the number of centuries and number of years. |
| const int calendarYear = utcTime.tm_year + kTmYearOffset; |
| const uint8_t year = calendarYear % kCentury; |
| const uint8_t century = calendarYear / kCentury; |
| |
| const Timestamp timestamp{ |
| .seconds = convertToBcd(utcTime.tm_sec), |
| .minutes = convertToBcd(utcTime.tm_min), |
| .hours = convertToBcd(utcTime.tm_hour), |
| .isPrecise = isPreciseBit, |
| .day = convertToBcd(utcTime.tm_mday), |
| // tm_mon is months since January, add one to get current month. |
| .month = convertToBcd(utcTime.tm_mon + 1), |
| .year = convertToBcd(year), |
| .century = convertToBcd(century)}; |
| |
| uint64_t ret; |
| memcpy(&ret, ×tamp, sizeof(ret)); |
| return ret; |
| } |
| |
| template <ByteLike ByteType> |
| class SeqMemcopy |
| { |
| public: |
| // Prevent the class from outliving the baseDest pointer. |
| SeqMemcopy() = delete; |
| SeqMemcopy(const SeqMemcopy&) = delete; |
| SeqMemcopy& operator=(const SeqMemcopy&) = delete; |
| |
| /** |
| * @brief Manages sequentially copying chunks of data to a contiguous memory |
| * block. |
| * |
| * @param[in] baseDest - The memory location to copy to. |
| */ |
| SeqMemcopy(std::span<ByteType> baseDest) : baseDest_(baseDest), offset_(0) |
| {} |
| |
| /** |
| * @brief Copies the src to the next section in the dest address. |
| * |
| * @param[in] src - The source address to copy from. |
| * @param[in] srcSize - The source size. |
| * |
| * @throws out_of_range If source size goes out of the destinations bounds. |
| */ |
| void copy(const void* src, const uint64_t srcSize) |
| { |
| if ((offset_ + srcSize) > baseDest_.size_bytes()) |
| { |
| throw std::out_of_range( |
| "Attempt to copy out of the destinations bounds"); |
| } |
| |
| memcpy(baseDest_.data() + offset_, src, srcSize); |
| offset_ += srcSize; |
| } |
| |
| private: |
| std::span<ByteType> baseDest_; |
| uint64_t offset_; |
| }; |
| |
| template <ByteLike BodyType> |
| class CperEncoder |
| { |
| public: |
| CperEncoder() : totalSectionBodySizeBytes_(0) |
| {} |
| |
| /** |
| * @brief Implementation specific CPER encoders must define how to create |
| * RecordHeaders. |
| * https://uefi.org/specs/UEFI/2.10/Apx_N_Common_Platform_Error_Record.html#record-header |
| * |
| * @param[in] recordLength - The length in bytes of the entire record. |
| * @param[in] sectionCount - The number of sections in the record. |
| */ |
| virtual RecordHeader createRecordHeader(const uint32_t recordLength, |
| const uint16_t sectionCount) = 0; |
| |
| /** |
| * @brief Implementation specific CPER encoders must define how to create |
| * new sectionDescriptors. |
| * https://uefi.org/specs/UEFI/2.10/Apx_N_Common_Platform_Error_Record.html#section-descriptor |
| * |
| * @param[in] sectionLength - The length in bytes of the section body. |
| * @param[in] sectionOffset - The offset in bytes from the section body from |
| * the base of the record header. |
| * @param[in] sectionFlags - Information that describes the error section. |
| * @param[in] sectionType - The ID of the sectionType; useful for |
| * implementations with multiple section types. |
| * |
| * @return A SectionDescriptor struct. |
| */ |
| virtual SectionDescriptor createSectionDescriptor( |
| const uint32_t sectionLength, const uint32_t sectionOffset, |
| const uint32_t sectionFlags, const guid_t sectionType) = 0; |
| |
| /** |
| * @brief Adds a new CPER section for the encoder to track. |
| * |
| * @note Cper logs have a max size of 4GiB including the header and |
| * descriptors. |
| * |
| * @param[in] sectionBody - A span containing the section data as a byte |
| * array. |
| * @param[in] sectionFlags - The CPER flags for the section. |
| * @param[in] sectionType - The guid_t for the section. |
| * |
| * @warning The CperEncoder class does not own the ptrs of the |
| * sectionBodies, it merely tracks the addresses and size. It is expected |
| * that the data added to the encoder exists for the entire lifetime of the |
| * CperEncoder object. |
| * |
| * @returns true on success, false if new section goes over the 4GiB |
| * threshold. |
| */ |
| bool addSection(const std::span<const BodyType> sectionBody, |
| const uint32_t sectionFlags, const guid_t sectionType) |
| { |
| if ((calculateRecordLengthBytes() + sectionBody.size_bytes() + |
| kSectionDescriptorSizeBytes) > |
| std::numeric_limits<uint32_t>::max()) |
| { |
| return false; |
| } |
| |
| sectionSeeds_.emplace_back(sectionFlags, sectionType, sectionBody); |
| totalSectionBodySizeBytes_ += sectionBody.size_bytes(); |
| return true; |
| } |
| |
| /** |
| * @brief Serializes the CPER Log into a vector of bytes. |
| * |
| * @return A byte array containing the CPER log contents. |
| */ |
| std::vector<uint8_t> serializeToByteArray() |
| { |
| const uint64_t totalBytes = calculateRecordLengthBytes(); |
| std::vector<uint8_t> byteArray(totalBytes); |
| |
| encode<uint8_t>(byteArray); |
| return byteArray; |
| } |
| |
| /** |
| * @brief Serializes the CPER Log into a string. |
| * |
| * @return A string containing the CPER log contents. |
| */ |
| std::string serializeToString() |
| { |
| const uint64_t totalBytes = calculateRecordLengthBytes(); |
| std::string str(totalBytes, 0); |
| |
| encode<char>(str); |
| return str; |
| } |
| |
| private: |
| // Save ~3x memory by only caching the section "seeds" and keeping a |
| // single full sectionDescriptor on the stack at a time while encoding |
| // the log. |
| struct SectionSeed |
| { |
| uint32_t sectionFlags; |
| guid_t sectionType; |
| std::span<const BodyType> body; |
| }; |
| |
| /** |
| * @brief Calculates the total size of the CPER log. |
| * |
| * @return The total size in bytes. |
| */ |
| uint32_t calculateRecordLengthBytes() const |
| { |
| return kRecordHeaderSizeBytes + |
| (sectionSeeds_.size() * kSectionDescriptorSizeBytes) + |
| totalSectionBodySizeBytes_; |
| } |
| |
| /** |
| * @brief Encodes the CPER byte array to the destination pointer. |
| * |
| * @tparam ByteType - Allows for different cpp byte representations (char, |
| * unsigned char, std::byte, etc.) |
| * |
| * @param[in,out] dest - The destination span to copy the data to. |
| * |
| * @throws If unable to successfully encode the CPER log. |
| */ |
| template <ByteLike ByteType> |
| void encode(std::span<ByteType> dest) |
| { |
| SeqMemcopy seqMemcpy(dest); |
| { |
| const RecordHeader header = |
| createRecordHeader(dest.size_bytes(), sectionSeeds_.size()); |
| |
| seqMemcpy.copy(&header, kRecordHeaderSizeBytes); |
| } |
| |
| // Offset in bytes of the section body from the base of the record |
| // header. |
| uint64_t sectionOffset = |
| kRecordHeaderSizeBytes + |
| (sectionSeeds_.size() * kSectionDescriptorSizeBytes); |
| |
| for (const SectionSeed& seed : sectionSeeds_) |
| { |
| // Creates a struct with the following modifiable fields - note this |
| // is a temporary struct which is then sequentially copied to a |
| // contiguous array. |
| const SectionDescriptor descriptor = |
| createSectionDescriptor(seed.body.size_bytes(), sectionOffset, |
| seed.sectionFlags, seed.sectionType); |
| sectionOffset += seed.body.size_bytes(); |
| |
| seqMemcpy.copy(&descriptor, kSectionDescriptorSizeBytes); |
| } |
| |
| for (const auto& seed : sectionSeeds_) |
| { |
| seqMemcpy.copy(seed.body.data(), seed.body.size_bytes()); |
| } |
| } |
| |
| uint32_t totalSectionBodySizeBytes_; |
| std::vector<SectionSeed> sectionSeeds_; |
| }; |
| } // namespace uefi::cper |