| | | 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 | | |
| | | 24 | | namespace holofile { |
| | | 25 | | |
| | 0 | 26 | | Exception::Exception(const std::string &message) : std::runtime_error(message) {} |
| | | 27 | | |
| | 0 | 28 | | Exception::~Exception() noexcept = default; |
| | | 29 | | |
| | 0 | 30 | | EndOfFileException::EndOfFileException() : Exception("Holofile: End of file reached") {} |
| | | 31 | | |
| | 0 | 32 | | IncompleteHeaderException::IncompleteHeaderException() : Exception("Holofile: Incomplete header") {} |
| | | 33 | | |
| | | 34 | | InvalidMagicNumberException::InvalidMagicNumberException() |
| | 0 | 35 | | : Exception("Holofile: Invalid magic number") {} |
| | | 36 | | |
| | 0 | 37 | | InvalidVersionException::InvalidVersionException() : Exception("Holofile: Invalid version") {} |
| | | 38 | | |
| | | 39 | | InvalidFrameSizeException::InvalidFrameSizeException() |
| | 0 | 40 | | : Exception("Holofile: Invalid frame size") {} |
| | | 41 | | |
| | 0 | 42 | | InvalidFooterException::InvalidFooterException() : Exception("Holofile: Invalid footer") {} |
| | | 43 | | |
| | | 44 | | // ------------------------------------------------------------------------------------------------- |
| | | 45 | | // Private implementation |
| | | 46 | | // ------------------------------------------------------------------------------------------------- |
| | | 47 | | |
| | | 48 | | namespace { |
| | | 49 | | |
| | | 50 | | struct FileCloser { |
| | 0 | 51 | | void operator()(FILE *file) const { fclose(file); } |
| | | 52 | | }; |
| | | 53 | | |
| | | 54 | | } // namespace |
| | | 55 | | |
| | | 56 | | struct 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 | | |
| | | 65 | | struct Writer::Impl { |
| | | 66 | | std::unique_ptr<FILE, FileCloser> file; |
| | | 67 | | Header header; |
| | | 68 | | Footer footer; |
| | | 69 | | std::size_t frame_index = 0; |
| | | 70 | | }; |
| | | 71 | | |
| | 0 | 72 | | Reader::Reader(const std::string &path) : impl_(std::make_unique<Impl>()) { |
| | | 73 | | // Open file |
| | 0 | 74 | | FILE *fp = nullptr; |
| | 0 | 75 | | if (fopen_s(&fp, path.c_str(), "rb") != 0 || !fp) { |
| | 0 | 76 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 77 | | throw std::system_error(ec, "Failed to open \"" + path + "\""); |
| | | 78 | | } |
| | 0 | 79 | | impl_->file.reset(fp); |
| | | 80 | | |
| | | 81 | | // Read header |
| | 0 | 82 | | std::size_t success = fread(&impl_->header, sizeof(impl_->header), 1, impl_->file.get()); |
| | 0 | 83 | | if (ferror(impl_->file.get())) { |
| | 0 | 84 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 85 | | throw std::system_error(ec, "Failed to read header:"); |
| | | 86 | | } |
| | 0 | 87 | | if (feof(impl_->file.get()) != 0) { |
| | 0 | 88 | | throw IncompleteHeaderException(); |
| | | 89 | | } |
| | | 90 | | |
| | 0 | 91 | | if (!success) { |
| | 0 | 92 | | logger()->critical("Unrecoverable error: fread() failed to read the " |
| | | 93 | | "header"); |
| | 0 | 94 | | std::exit(EXIT_FAILURE); |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | // Check the header |
| | 0 | 98 | | uint32_t magic_number = std::endian::native == std::endian::little ? 0x4F4C4F48 : 0x484F4C4F; |
| | 0 | 99 | | if (impl_->header.magic_number != magic_number) { |
| | 0 | 100 | | throw InvalidMagicNumberException(); |
| | | 101 | | } |
| | | 102 | | |
| | | 103 | | // TODO: Version support. |
| | | 104 | | |
| | 0 | 105 | | std::size_t pixel_per_frame = impl_->header.frame_width * impl_->header.frame_height; |
| | 0 | 106 | | std::size_t bits_per_frame = pixel_per_frame * impl_->header.bits_per_pixel; |
| | 0 | 107 | | if (bits_per_frame % 8 != 0) { |
| | 0 | 108 | | throw InvalidFrameSizeException(); |
| | | 109 | | } |
| | | 110 | | |
| | | 111 | | try { |
| | 0 | 112 | | impl_->read_footer(); |
| | 0 | 113 | | } catch (const Exception &e) { |
| | 0 | 114 | | logger()->warn("Holofile footer could not be read: {}", e.what()); |
| | 0 | 115 | | impl_->footer = std::nullopt; |
| | | 116 | | |
| | 0 | 117 | | if (fseek(impl_->file.get(), sizeof(impl_->header), SEEK_SET) != 0) { |
| | 0 | 118 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 119 | | throw std::system_error(ec, "Failed to seek:"); |
| | | 120 | | } |
| | 0 | 121 | | } |
| | 0 | 122 | | } |
| | | 123 | | |
| | 0 | 124 | | Reader::~Reader() = default; |
| | | 125 | | |
| | 0 | 126 | | Reader::Reader(Reader &&) noexcept = default; |
| | | 127 | | |
| | 0 | 128 | | Reader &Reader::operator=(Reader &&) noexcept = default; |
| | | 129 | | |
| | 0 | 130 | | const Header &Reader::header() const { return impl_->header; } |
| | | 131 | | |
| | 0 | 132 | | std::optional<Footer> Reader::footer() const { return impl_->footer; } |
| | | 133 | | |
| | 0 | 134 | | void Reader::Impl::read_footer() { |
| | 0 | 135 | | size_t footer_offset = sizeof(Header) + header.data_size_in_bytes; |
| | | 136 | | |
| | 0 | 137 | | int64_t current_pos = _ftelli64(file.get()); |
| | 0 | 138 | | if (current_pos == -1) { |
| | 0 | 139 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 140 | | throw std::system_error(ec, "Failed to get current file position:"); |
| | | 141 | | } |
| | | 142 | | |
| | 0 | 143 | | if (_fseeki64(file.get(), 0, SEEK_END) != 0) { |
| | 0 | 144 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 145 | | throw std::system_error(ec, "Failed to seek to end:"); |
| | | 146 | | } |
| | 0 | 147 | | int64_t file_size = _ftelli64(file.get()); |
| | 0 | 148 | | if (file_size == -1) { |
| | 0 | 149 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 150 | | throw std::system_error(ec, "Failed to get file size:"); |
| | | 151 | | } |
| | | 152 | | |
| | 0 | 153 | | if (static_cast<size_t>(file_size) <= footer_offset) { |
| | 0 | 154 | | logger()->info("No footer found - file ends at data section"); |
| | 0 | 155 | | throw InvalidFooterException(); |
| | | 156 | | } |
| | | 157 | | |
| | 0 | 158 | | size_t footer_size = file_size - footer_offset; |
| | | 159 | | |
| | 0 | 160 | | if (footer_size > 1024 * 1024) { |
| | 0 | 161 | | logger()->warn("Footer appears too large ({} bytes), likely not a valid footer", footer_size); |
| | 0 | 162 | | throw InvalidFooterException(); |
| | | 163 | | } |
| | | 164 | | |
| | 0 | 165 | | if (_fseeki64(file.get(), static_cast<int64_t>(footer_offset), SEEK_SET) != 0) { |
| | 0 | 166 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 167 | | throw std::system_error(ec, "Failed to seek to footer:"); |
| | | 168 | | } |
| | | 169 | | |
| | 0 | 170 | | std::string footer_json; |
| | 0 | 171 | | footer_json.resize(footer_size); |
| | | 172 | | |
| | 0 | 173 | | size_t bytes_read = fread(footer_json.data(), 1, footer_size, file.get()); |
| | 0 | 174 | | if (bytes_read != footer_size) { |
| | 0 | 175 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 176 | | throw std::system_error(ec, "Failed to read footer:"); |
| | | 177 | | } |
| | | 178 | | |
| | 0 | 179 | | if (footer_json.empty() || footer_json[0] != '{') { |
| | 0 | 180 | | logger()->warn("Footer does not appear to be valid JSON (starts with '{}')", |
| | | 181 | | footer_json.empty() ? "empty" : std::string(1, footer_json[0])); |
| | 0 | 182 | | throw InvalidFooterException(); |
| | | 183 | | } |
| | | 184 | | |
| | | 185 | | try { |
| | 0 | 186 | | Footer parsed_footer; |
| | 0 | 187 | | parsed_footer.pipeline_settings = nlohmann::json::parse(footer_json); |
| | 0 | 188 | | footer = std::move(parsed_footer); |
| | 0 | 189 | | } catch (const nlohmann::json::exception &e) { |
| | 0 | 190 | | logger()->error("Failed to parse Holofile footer JSON: {}", e.what()); |
| | 0 | 191 | | std::string preview = footer_json.substr(0, std::min(footer_json.size(), size_t(100))); |
| | 0 | 192 | | logger()->debug("Footer content preview: {}", preview); |
| | 0 | 193 | | throw InvalidFooterException(); |
| | 0 | 194 | | } |
| | | 195 | | |
| | 0 | 196 | | if (_fseeki64(file.get(), current_pos, SEEK_SET) != 0) { |
| | 0 | 197 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 198 | | throw std::system_error(ec, "Failed to restore file position:"); |
| | | 199 | | } |
| | 0 | 200 | | } |
| | | 201 | | |
| | 0 | 202 | | void Reader::seek(std::size_t frame_index) { |
| | 0 | 203 | | size_t pixels_per_frame = impl_->header.frame_width * impl_->header.frame_height; |
| | 0 | 204 | | size_t bits_per_frame = pixels_per_frame * impl_->header.bits_per_pixel; |
| | 0 | 205 | | size_t bytes_per_frame = bits_per_frame / 8; |
| | | 206 | | |
| | 0 | 207 | | size_t offset = sizeof(impl_->header) + frame_index * bytes_per_frame; |
| | 0 | 208 | | if (_fseeki64(impl_->file.get(), static_cast<int64_t>(offset), SEEK_SET) != 0) { |
| | 0 | 209 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 210 | | throw std::system_error(ec, "Failed to seek:"); |
| | | 211 | | } |
| | | 212 | | |
| | 0 | 213 | | impl_->frame_index = frame_index; |
| | 0 | 214 | | } |
| | | 215 | | |
| | 0 | 216 | | std::size_t Reader::tell() const { return impl_->frame_index; } |
| | | 217 | | |
| | 0 | 218 | | void Reader::read_frames(uint8_t *data, std::size_t frame_count) { |
| | 0 | 219 | | size_t pixels_per_frame = impl_->header.frame_width * impl_->header.frame_height; |
| | 0 | 220 | | size_t bits_per_frame = pixels_per_frame * impl_->header.bits_per_pixel; |
| | 0 | 221 | | size_t bytes_per_frame = bits_per_frame / 8; |
| | | 222 | | |
| | 0 | 223 | | size_t frames_read = fread(data, bytes_per_frame, frame_count, impl_->file.get()); |
| | 0 | 224 | | impl_->frame_index += frames_read; |
| | | 225 | | |
| | 0 | 226 | | if (ferror(impl_->file.get())) { |
| | 0 | 227 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 228 | | throw std::system_error(ec, "Failed to read frames:"); |
| | | 229 | | } |
| | 0 | 230 | | if (frames_read != frame_count && feof(impl_->file.get()) != 0) { |
| | 0 | 231 | | throw EndOfFileException(); |
| | | 232 | | } |
| | 0 | 233 | | if (frames_read != frame_count) { |
| | 0 | 234 | | logger()->critical("Unrecoverable error: fread() failed to read the " |
| | | 235 | | "requested number of frames."); |
| | 0 | 236 | | std::exit(EXIT_FAILURE); |
| | | 237 | | } |
| | 0 | 238 | | } |
| | | 239 | | |
| | | 240 | | Writer::Writer(const std::string &path, const Header &header, const Footer &footer) |
| | 0 | 241 | | : impl_(std::make_unique<Impl>()) { |
| | 0 | 242 | | impl_->header = header; |
| | 0 | 243 | | impl_->footer = footer; |
| | | 244 | | // Open file |
| | 0 | 245 | | FILE *fp = nullptr; |
| | 0 | 246 | | if (fopen_s(&fp, path.c_str(), "wb") != 0 || !fp) { |
| | 0 | 247 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 248 | | throw std::system_error(ec, "Failed to open \"" + path + "\""); |
| | | 249 | | } |
| | 0 | 250 | | impl_->file.reset(fp); |
| | | 251 | | |
| | | 252 | | // Write header |
| | 0 | 253 | | std::size_t success = fwrite(&impl_->header, sizeof(impl_->header), 1, impl_->file.get()); |
| | 0 | 254 | | if (ferror(impl_->file.get())) { |
| | 0 | 255 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 256 | | throw std::system_error(ec, "Failed to write header:"); |
| | | 257 | | } |
| | | 258 | | |
| | 0 | 259 | | if (!success) { |
| | 0 | 260 | | logger()->critical("Unrecoverable error: fwrite() failed to write the " |
| | | 261 | | "header"); |
| | 0 | 262 | | std::exit(EXIT_FAILURE); |
| | | 263 | | } |
| | 0 | 264 | | } |
| | | 265 | | |
| | 0 | 266 | | Writer::~Writer() = default; |
| | | 267 | | |
| | 0 | 268 | | Writer::Writer(Writer &&) noexcept = default; |
| | | 269 | | |
| | 0 | 270 | | Writer &Writer::operator=(Writer &&) noexcept = default; |
| | | 271 | | |
| | 0 | 272 | | void Writer::write_footer() { |
| | 0 | 273 | | std::string footer_json = impl_->footer.pipeline_settings.dump(); |
| | | 274 | | |
| | 0 | 275 | | logger()->info("Writing Holofile footer with pipeline settings: {}", footer_json); |
| | | 276 | | |
| | 0 | 277 | | if (_fseeki64(impl_->file.get(), 0, SEEK_END) != 0) { |
| | 0 | 278 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 279 | | throw std::system_error(ec, "Failed to seek to end of file:"); |
| | | 280 | | } |
| | | 281 | | |
| | 0 | 282 | | if (fwrite(footer_json.data(), 1, footer_json.size(), impl_->file.get()) != footer_json.size()) { |
| | 0 | 283 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 284 | | throw std::system_error(ec, "Failed to write footer JSON:"); |
| | | 285 | | } |
| | | 286 | | |
| | 0 | 287 | | if (fflush(impl_->file.get()) != 0) { |
| | 0 | 288 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 289 | | throw std::system_error(ec, "Failed to flush file:"); |
| | | 290 | | } |
| | 0 | 291 | | } |
| | | 292 | | |
| | 0 | 293 | | void Writer::write_frames(const uint8_t *data, std::size_t frame_count) { |
| | 0 | 294 | | size_t pixels_per_frame = impl_->header.frame_width * impl_->header.frame_height; |
| | 0 | 295 | | size_t bits_per_frame = pixels_per_frame * impl_->header.bits_per_pixel; |
| | 0 | 296 | | size_t bytes_per_frame = bits_per_frame / 8; |
| | | 297 | | |
| | 0 | 298 | | size_t frames_written = fwrite(data, bytes_per_frame, frame_count, impl_->file.get()); |
| | 0 | 299 | | impl_->frame_index += frames_written; |
| | | 300 | | |
| | 0 | 301 | | if (ferror(impl_->file.get())) { |
| | 0 | 302 | | std::error_code ec(errno, std::generic_category()); |
| | 0 | 303 | | throw std::system_error(ec, "Failed to write frames:"); |
| | | 304 | | } |
| | 0 | 305 | | if (frames_written != frame_count) { |
| | 0 | 306 | | logger()->critical("Unrecoverable error: fwrite() failed to write the " |
| | | 307 | | "requested number of frames."); |
| | 0 | 308 | | std::exit(EXIT_FAILURE); |
| | | 309 | | } |
| | 0 | 310 | | } |
| | | 311 | | |
| | 0 | 312 | | size_t Writer::tell() const { return impl_->frame_index; } |
| | | 313 | | |
| | | 314 | | } // namespace holofile |