File indexing completed on 2026-09-16 08:21:53
0001
0002
0003
0004
0005
0006
0007
0008
0009 #include "ActsPlugins/Json/detail/JsonIo.hpp"
0010
0011 #include <algorithm>
0012 #include <array>
0013 #include <format>
0014 #include <fstream>
0015 #include <stdexcept>
0016 #include <string>
0017 #include <string_view>
0018
0019 #ifdef ACTS_JSON_ZSTD_SUPPORT
0020 #include <zstd.h>
0021 #endif
0022
0023 namespace {
0024
0025 using Acts::detail::JsonCompression;
0026 using Acts::detail::JsonEncoding;
0027 using Acts::detail::JsonFileFormat;
0028
0029
0030 constexpr std::array<std::byte, 4> kZstdMagic{std::byte{0x28}, std::byte{0xB5},
0031 std::byte{0x2F}, std::byte{0xFD}};
0032
0033 bool hasZstdMagic(std::span<const std::byte> data) {
0034 return data.size() >= kZstdMagic.size() &&
0035 std::equal(kZstdMagic.begin(), kZstdMagic.end(), data.begin());
0036 }
0037
0038
0039 template <typename container_t>
0040 std::vector<std::byte> toBytes(const container_t& container) {
0041 auto bytes = std::as_bytes(std::span{container});
0042 return {bytes.begin(), bytes.end()};
0043 }
0044
0045 std::string_view asStringView(std::span<const std::byte> data) {
0046 return {reinterpret_cast<const char*>(data.data()), data.size()};
0047 }
0048
0049 std::vector<std::byte> compressZstd(std::span<const std::byte> data,
0050 int level) {
0051 #ifdef ACTS_JSON_ZSTD_SUPPORT
0052 std::vector<std::byte> out(ZSTD_compressBound(data.size()));
0053 std::size_t written =
0054 ZSTD_compress(out.data(), out.size(), data.data(), data.size(), level);
0055 if (ZSTD_isError(written) != 0u) {
0056 throw std::runtime_error(
0057 std::format("zstd compression failed: {}", ZSTD_getErrorName(written)));
0058 }
0059 out.resize(written);
0060 return out;
0061 #else
0062 static_cast<void>(data);
0063 static_cast<void>(level);
0064 throw std::runtime_error(
0065 "zstd compressed output was requested, but ACTS was built without zstd "
0066 "support. Reconfigure with zstd available to write compressed files.");
0067 #endif
0068 }
0069
0070 std::vector<std::byte> decompressZstd(std::span<const std::byte> data,
0071 const std::filesystem::path& origin) {
0072 #ifdef ACTS_JSON_ZSTD_SUPPORT
0073 unsigned long long size = ZSTD_getFrameContentSize(data.data(), data.size());
0074 if (size == ZSTD_CONTENTSIZE_ERROR) {
0075 throw std::runtime_error(std::format(
0076 "'{}' starts with a zstd magic number but is not a valid zstd frame",
0077 origin.string()));
0078 }
0079 if (size == ZSTD_CONTENTSIZE_UNKNOWN) {
0080 throw std::runtime_error(std::format(
0081 "'{}' is a zstd stream without a declared content size, which is not "
0082 "supported here; recompress it as a single frame",
0083 origin.string()));
0084 }
0085 std::vector<std::byte> out(size);
0086 std::size_t written =
0087 ZSTD_decompress(out.data(), out.size(), data.data(), data.size());
0088 if (ZSTD_isError(written) != 0u) {
0089 throw std::runtime_error(
0090 std::format("'{}' could not be decompressed, the zstd frame is likely "
0091 "truncated or corrupt: {}",
0092 origin.string(), ZSTD_getErrorName(written)));
0093 }
0094 out.resize(written);
0095 return out;
0096 #else
0097 static_cast<void>(data);
0098 throw std::runtime_error(std::format(
0099 "'{}' is zstd compressed, but ACTS was built without zstd support. "
0100 "Reconfigure with zstd available to read this file.",
0101 origin.string()));
0102 #endif
0103 }
0104
0105 }
0106
0107 bool Acts::detail::zstdSupported() {
0108 #ifdef ACTS_JSON_ZSTD_SUPPORT
0109 return true;
0110 #else
0111 return false;
0112 #endif
0113 }
0114
0115 Acts::detail::JsonFileFormat Acts::detail::jsonFormatFromPath(
0116 const std::filesystem::path& path) {
0117 std::string name = path.filename().string();
0118 JsonFileFormat format{};
0119
0120 if (name.ends_with(".zst")) {
0121 format.compression = JsonCompression::Zstd;
0122 name.resize(name.size() - std::string_view{".zst"}.size());
0123 }
0124
0125 if (name.ends_with(".json")) {
0126 format.encoding = JsonEncoding::Text;
0127 } else if (name.ends_with(".cbor")) {
0128 format.encoding = JsonEncoding::Cbor;
0129 } else {
0130 throw std::invalid_argument(std::format(
0131 "Cannot deduce the output format of '{}': expected one of '.json', "
0132 "'.cbor', '.json.zst' or '.cbor.zst'",
0133 path.string()));
0134 }
0135
0136 return format;
0137 }
0138
0139 std::vector<std::byte> Acts::detail::encodeJson(const nlohmann::json& payload,
0140 const JsonFileFormat& format,
0141 unsigned indentation,
0142 int compressionLevel) {
0143 std::vector<std::byte> encoded =
0144 format.encoding == JsonEncoding::Cbor
0145 ? toBytes(nlohmann::json::to_cbor(payload))
0146 : toBytes(payload.dump(static_cast<int>(indentation)));
0147
0148 if (format.compression == JsonCompression::Zstd) {
0149 return compressZstd(encoded, compressionLevel);
0150 }
0151 return encoded;
0152 }
0153
0154 nlohmann::json Acts::detail::decodeJson(std::span<const std::byte> data,
0155 const std::filesystem::path& origin) {
0156 std::vector<std::byte> decompressed;
0157 bool wasCompressed = hasZstdMagic(data);
0158 if (wasCompressed) {
0159 decompressed = decompressZstd(data, origin);
0160 data = decompressed;
0161 }
0162
0163 auto isSpace = [](std::byte b) {
0164 char c = static_cast<char>(b);
0165 return c == ' ' || c == '\t' || c == '\n' || c == '\r';
0166 };
0167 auto it = std::ranges::find_if_not(data, isSpace);
0168 if (it == data.end()) {
0169 throw std::runtime_error(
0170 std::format("'{}' contains no payload to decode", origin.string()));
0171 }
0172
0173
0174
0175
0176 auto lead = static_cast<unsigned char>(*it);
0177 bool isText = lead == '{' || lead == '[';
0178 bool isCbor = (lead & 0xE0) == 0xA0 || (lead & 0xE0) == 0x80;
0179
0180
0181
0182 std::string_view stage = wasCompressed ? " (after decompression)" : "";
0183
0184 if (!isText && !isCbor) {
0185 throw std::runtime_error(std::format(
0186 "'{}'{} is neither JSON text nor CBOR, leading byte is 0x{:02X}",
0187 origin.string(), stage, lead));
0188 }
0189
0190 try {
0191 if (isText) {
0192 return nlohmann::json::parse(asStringView(data));
0193 }
0194 return nlohmann::json::from_cbor(data);
0195 } catch (const nlohmann::json::exception& e) {
0196 throw std::runtime_error(std::format(
0197 "Failed to decode {} payload from '{}'{}: {}", isText ? "JSON" : "CBOR",
0198 origin.string(), stage, e.what()));
0199 }
0200 }
0201
0202 void Acts::detail::writeJsonFile(const std::filesystem::path& path,
0203 const nlohmann::json& payload,
0204 unsigned indentation, int compressionLevel) {
0205 std::vector<std::byte> encoded = encodeJson(payload, jsonFormatFromPath(path),
0206 indentation, compressionLevel);
0207
0208 std::ofstream ofs{path, std::ios::binary};
0209 if (!ofs.good()) {
0210 throw std::runtime_error(
0211 std::format("Cannot open '{}' for writing", path.string()));
0212 }
0213 ofs.write(reinterpret_cast<const char*>(encoded.data()),
0214 static_cast<std::streamsize>(encoded.size()));
0215 if (!ofs.good()) {
0216 throw std::runtime_error(
0217 std::format("Failed to write '{}'", path.string()));
0218 }
0219 }
0220
0221 nlohmann::json Acts::detail::readJsonFile(const std::filesystem::path& path) {
0222 if (!std::filesystem::exists(path)) {
0223 throw std::invalid_argument(
0224 std::format("File '{}' does not exist", path.string()));
0225 }
0226
0227 std::ifstream ifs{path, std::ios::binary};
0228 if (!ifs.good()) {
0229 throw std::invalid_argument(std::format("Cannot open '{}'", path.string()));
0230 }
0231
0232 ifs.seekg(0, std::ios::end);
0233 auto size = ifs.tellg();
0234 if (size < 0) {
0235 throw std::runtime_error(
0236 std::format("Cannot determine the size of '{}'", path.string()));
0237 }
0238 ifs.seekg(0, std::ios::beg);
0239
0240 std::vector<std::byte> data(static_cast<std::size_t>(size));
0241 ifs.read(reinterpret_cast<char*>(data.data()),
0242 static_cast<std::streamsize>(data.size()));
0243 if (ifs.bad()) {
0244 throw std::runtime_error(std::format("Failed to read '{}'", path.string()));
0245 }
0246
0247 return decodeJson(data, path);
0248 }