< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 167
Coverable lines: 167
Total lines: 352
Line coverage: 0%
Branch coverage
N/A
Covered branches: 0
Total branches: 0
Branch coverage: N/A
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

File(s)

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holofile\src\holofile.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "holofile/holofile.hh"
 16
 17#include <bit>
 18#include <cstdio>
 19#include <system_error>
 20#include <utility>
 21
 22#include "logger.hh"
 23
 24namespace holofile {
 25
 026Exception::Exception(const std::string &message) : std::runtime_error(message) {}
 27
 028Exception::~Exception() noexcept = default;
 29
 030EndOfFileException::EndOfFileException() : Exception("Holofile: End of file reached") {}
 31
 032IncompleteHeaderException::IncompleteHeaderException() : Exception("Holofile: Incomplete header") {}
 33
 34InvalidMagicNumberException::InvalidMagicNumberException()
 035    : Exception("Holofile: Invalid magic number") {}
 36
 037InvalidVersionException::InvalidVersionException() : Exception("Holofile: Invalid version") {}
 38
 39InvalidFrameSizeException::InvalidFrameSizeException()
 040    : Exception("Holofile: Invalid frame size") {}
 41
 042InvalidFooterException::InvalidFooterException() : Exception("Holofile: Invalid footer") {}
 43
 44// -------------------------------------------------------------------------------------------------
 45// Private implementation
 46// -------------------------------------------------------------------------------------------------
 47
 48namespace {
 49
 50struct FileCloser {
 051  void operator()(FILE *file) const { fclose(file); }
 52};
 53
 54} // namespace
 55
 56struct Reader::Impl {
 57  void read_footer();
 58
 59  std::unique_ptr<FILE, FileCloser> file;
 60  Header                            header;
 61  std::optional<Footer>             footer;
 62  std::size_t                       frame_index = 0;
 63};
 64
 65struct Writer::Impl {
 66  std::unique_ptr<FILE, FileCloser> file;
 67  Header                            header;
 68  Footer                            footer;
 69  std::size_t                       frame_index = 0;
 70};
 71
 072Reader::Reader(const std::string &path) : impl_(std::make_unique<Impl>()) {
 73  // Open file
 074  FILE *fp = nullptr;
 075  if (fopen_s(&fp, path.c_str(), "rb") != 0 || !fp) {
 076    std::error_code ec(errno, std::generic_category());
 077    throw std::system_error(ec, "Failed to open \"" + path + "\"");
 78  }
 079  impl_->file.reset(fp);
 80
 81  // Read header
 082  std::size_t success = fread(&impl_->header, sizeof(impl_->header), 1, impl_->file.get());
 083  if (ferror(impl_->file.get())) {
 084    std::error_code ec(errno, std::generic_category());
 085    throw std::system_error(ec, "Failed to read header:");
 86  }
 087  if (feof(impl_->file.get()) != 0) {
 088    throw IncompleteHeaderException();
 89  }
 90
 091  if (!success) {
 092    logger()->critical("Unrecoverable error: fread() failed to read the "
 93                       "header");
 094    std::exit(EXIT_FAILURE);
 95  }
 96
 97  // Check the header
 098  uint32_t magic_number = std::endian::native == std::endian::little ? 0x4F4C4F48 : 0x484F4C4F;
 099  if (impl_->header.magic_number != magic_number) {
 0100    throw InvalidMagicNumberException();
 101  }
 102
 103  // TODO: Version support.
 104
 0105  std::size_t pixel_per_frame = impl_->header.frame_width * impl_->header.frame_height;
 0106  std::size_t bits_per_frame  = pixel_per_frame * impl_->header.bits_per_pixel;
 0107  if (bits_per_frame % 8 != 0) {
 0108    throw InvalidFrameSizeException();
 109  }
 110
 111  try {
 0112    impl_->read_footer();
 0113  } catch (const Exception &e) {
 0114    logger()->warn("Holofile footer could not be read: {}", e.what());
 0115    impl_->footer = std::nullopt;
 116
 0117    if (fseek(impl_->file.get(), sizeof(impl_->header), SEEK_SET) != 0) {
 0118      std::error_code ec(errno, std::generic_category());
 0119      throw std::system_error(ec, "Failed to seek:");
 120    }
 0121  }
 0122}
 123
 0124Reader::~Reader() = default;
 125
 0126Reader::Reader(Reader &&) noexcept = default;
 127
 0128Reader &Reader::operator=(Reader &&) noexcept = default;
 129
 0130const Header &Reader::header() const { return impl_->header; }
 131
 0132std::optional<Footer> Reader::footer() const { return impl_->footer; }
 133
 0134void Reader::Impl::read_footer() {
 0135  size_t footer_offset = sizeof(Header) + header.data_size_in_bytes;
 136
 0137  int64_t current_pos = _ftelli64(file.get());
 0138  if (current_pos == -1) {
 0139    std::error_code ec(errno, std::generic_category());
 0140    throw std::system_error(ec, "Failed to get current file position:");
 141  }
 142
 0143  if (_fseeki64(file.get(), 0, SEEK_END) != 0) {
 0144    std::error_code ec(errno, std::generic_category());
 0145    throw std::system_error(ec, "Failed to seek to end:");
 146  }
 0147  int64_t file_size = _ftelli64(file.get());
 0148  if (file_size == -1) {
 0149    std::error_code ec(errno, std::generic_category());
 0150    throw std::system_error(ec, "Failed to get file size:");
 151  }
 152
 0153  if (static_cast<size_t>(file_size) <= footer_offset) {
 0154    logger()->info("No footer found - file ends at data section");
 0155    throw InvalidFooterException();
 156  }
 157
 0158  size_t footer_size = file_size - footer_offset;
 159
 0160  if (footer_size > 1024 * 1024) {
 0161    logger()->warn("Footer appears too large ({} bytes), likely not a valid footer", footer_size);
 0162    throw InvalidFooterException();
 163  }
 164
 0165  if (_fseeki64(file.get(), static_cast<int64_t>(footer_offset), SEEK_SET) != 0) {
 0166    std::error_code ec(errno, std::generic_category());
 0167    throw std::system_error(ec, "Failed to seek to footer:");
 168  }
 169
 0170  std::string footer_json;
 0171  footer_json.resize(footer_size);
 172
 0173  size_t bytes_read = fread(footer_json.data(), 1, footer_size, file.get());
 0174  if (bytes_read != footer_size) {
 0175    std::error_code ec(errno, std::generic_category());
 0176    throw std::system_error(ec, "Failed to read footer:");
 177  }
 178
 0179  if (footer_json.empty() || footer_json[0] != '{') {
 0180    logger()->warn("Footer does not appear to be valid JSON (starts with '{}')",
 181                   footer_json.empty() ? "empty" : std::string(1, footer_json[0]));
 0182    throw InvalidFooterException();
 183  }
 184
 185  try {
 0186    Footer parsed_footer;
 0187    parsed_footer.pipeline_settings = nlohmann::json::parse(footer_json);
 0188    footer                          = std::move(parsed_footer);
 0189  } catch (const nlohmann::json::exception &e) {
 0190    logger()->error("Failed to parse Holofile footer JSON: {}", e.what());
 0191    std::string preview = footer_json.substr(0, std::min(footer_json.size(), size_t(100)));
 0192    logger()->debug("Footer content preview: {}", preview);
 0193    throw InvalidFooterException();
 0194  }
 195
 0196  if (_fseeki64(file.get(), current_pos, SEEK_SET) != 0) {
 0197    std::error_code ec(errno, std::generic_category());
 0198    throw std::system_error(ec, "Failed to restore file position:");
 199  }
 0200}
 201
 0202void Reader::seek(std::size_t frame_index) {
 0203  size_t pixels_per_frame = impl_->header.frame_width * impl_->header.frame_height;
 0204  size_t bits_per_frame   = pixels_per_frame * impl_->header.bits_per_pixel;
 0205  size_t bytes_per_frame  = bits_per_frame / 8;
 206
 0207  size_t offset = sizeof(impl_->header) + frame_index * bytes_per_frame;
 0208  if (_fseeki64(impl_->file.get(), static_cast<int64_t>(offset), SEEK_SET) != 0) {
 0209    std::error_code ec(errno, std::generic_category());
 0210    throw std::system_error(ec, "Failed to seek:");
 211  }
 212
 0213  impl_->frame_index = frame_index;
 0214}
 215
 0216std::size_t Reader::tell() const { return impl_->frame_index; }
 217
 0218void Reader::read_frames(uint8_t *data, std::size_t frame_count) {
 0219  size_t pixels_per_frame = impl_->header.frame_width * impl_->header.frame_height;
 0220  size_t bits_per_frame   = pixels_per_frame * impl_->header.bits_per_pixel;
 0221  size_t bytes_per_frame  = bits_per_frame / 8;
 222
 0223  size_t frames_read = fread(data, bytes_per_frame, frame_count, impl_->file.get());
 0224  impl_->frame_index += frames_read;
 225
 0226  if (ferror(impl_->file.get())) {
 0227    std::error_code ec(errno, std::generic_category());
 0228    throw std::system_error(ec, "Failed to read frames:");
 229  }
 0230  if (frames_read != frame_count && feof(impl_->file.get()) != 0) {
 0231    throw EndOfFileException();
 232  }
 0233  if (frames_read != frame_count) {
 0234    logger()->critical("Unrecoverable error: fread() failed to read the "
 235                       "requested number of frames.");
 0236    std::exit(EXIT_FAILURE);
 237  }
 0238}
 239
 240Writer::Writer(const std::string &path, const Header &header, const Footer &footer)
 0241    : impl_(std::make_unique<Impl>()) {
 0242  impl_->header = header;
 0243  impl_->footer = footer;
 244  // Open file
 0245  FILE *fp = nullptr;
 0246  if (fopen_s(&fp, path.c_str(), "wb") != 0 || !fp) {
 0247    std::error_code ec(errno, std::generic_category());
 0248    throw std::system_error(ec, "Failed to open \"" + path + "\"");
 249  }
 0250  impl_->file.reset(fp);
 251
 252  // Write header
 0253  std::size_t success = fwrite(&impl_->header, sizeof(impl_->header), 1, impl_->file.get());
 0254  if (ferror(impl_->file.get())) {
 0255    std::error_code ec(errno, std::generic_category());
 0256    throw std::system_error(ec, "Failed to write header:");
 257  }
 258
 0259  if (!success) {
 0260    logger()->critical("Unrecoverable error: fwrite() failed to write the "
 261                       "header");
 0262    std::exit(EXIT_FAILURE);
 263  }
 0264}
 265
 0266Writer::~Writer() = default;
 267
 0268Writer::Writer(Writer &&) noexcept = default;
 269
 0270Writer &Writer::operator=(Writer &&) noexcept = default;
 271
 0272void Writer::write_footer() {
 0273  std::string footer_json = impl_->footer.pipeline_settings.dump();
 274
 0275  logger()->info("Writing Holofile footer with pipeline settings: {}", footer_json);
 276
 0277  if (_fseeki64(impl_->file.get(), 0, SEEK_END) != 0) {
 0278    std::error_code ec(errno, std::generic_category());
 0279    throw std::system_error(ec, "Failed to seek to end of file:");
 280  }
 281
 0282  if (fwrite(footer_json.data(), 1, footer_json.size(), impl_->file.get()) != footer_json.size()) {
 0283    std::error_code ec(errno, std::generic_category());
 0284    throw std::system_error(ec, "Failed to write footer JSON:");
 285  }
 286
 0287  if (fflush(impl_->file.get()) != 0) {
 0288    std::error_code ec(errno, std::generic_category());
 0289    throw std::system_error(ec, "Failed to flush file:");
 290  }
 0291}
 292
 0293void Writer::write_frames(const uint8_t *data, std::size_t frame_count) {
 0294  size_t pixels_per_frame = impl_->header.frame_width * impl_->header.frame_height;
 0295  size_t bits_per_frame   = pixels_per_frame * impl_->header.bits_per_pixel;
 0296  size_t bytes_per_frame  = bits_per_frame / 8;
 297
 0298  size_t frames_written = fwrite(data, bytes_per_frame, frame_count, impl_->file.get());
 0299  impl_->frame_index += frames_written;
 300
 0301  if (ferror(impl_->file.get())) {
 0302    std::error_code ec(errno, std::generic_category());
 0303    throw std::system_error(ec, "Failed to write frames:");
 304  }
 0305  if (frames_written != frame_count) {
 0306    logger()->critical("Unrecoverable error: fwrite() failed to write the "
 307                       "requested number of frames.");
 0308    std::exit(EXIT_FAILURE);
 309  }
 0310}
 311
 0312size_t Writer::tell() const { return impl_->frame_index; }
 313
 314} // namespace holofile

C:\Users\Kremenchuk\actions-runner\_work\Holoflow\Holoflow\src\holofile\src\logger.cc

#LineLine coverage
 1// Copyright 2025 Digital Holography Foundation
 2//
 3// Licensed under the Apache License, Version 2.0 (the "License");
 4// you may not use this file except in compliance with the License.
 5// You may obtain a copy of the License at
 6//
 7//     http://www.apache.org/licenses/LICENSE-2.0
 8//
 9// Unless required by applicable law or agreed to in writing, software
 10// distributed under the License is distributed on an "AS IS" BASIS,
 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 12// See the License for the specific language governing permissions and
 13// limitations under the License.
 14
 15#include "logger.hh"
 16
 17#include <spdlog/sinks/stdout_color_sinks.h>
 18#include <spdlog/spdlog.h>
 19
 20namespace holofile {
 21
 022std::shared_ptr<spdlog::logger> logger() {
 023  static std::shared_ptr<spdlog::logger> logger = [] {
 24    auto sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
 25    sink->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%n] [thread %t] [%^%l%$] %v");
 26
 27    auto log = std::make_shared<spdlog::logger>("holofile", sink);
 28    log->set_level(spdlog::default_logger()->level());
 29    log->flush_on(spdlog::level::warn);
 30
 31    spdlog::register_logger(log);
 32
 33    return log;
 034  }();
 035  return logger;
 036}
 37
 38} // namespace holofile

Methods/Properties