libflute
File.cpp
Go to the documentation of this file.
1 // libflute - FLUTE/ALC library
2 //
3 // Copyright (C) 2021 Klaus Kühnhammer (Österreichische Rundfunksender GmbH & Co KG)
4 //
5 // Licensed under the License terms and conditions for use, reproduction, and
6 // distribution of 5G-MAG software (the “License”). You may not use this file
7 // except in compliance with the License. You may obtain a copy of the License at
8 // https://www.5g-mag.com/reference-tools. Unless required by applicable law or
9 // agreed to in writing, software distributed under the License is distributed on
10 // an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11 // or implied.
12 //
13 // See the License for the specific language governing permissions and limitations
14 // under the License.
15 //
16 #include <iostream>
17 #include <string>
18 #include <stdexcept>
19 #include <cstring>
20 #include <cmath>
21 #include <cassert>
22 #include <algorithm>
23 #include <sstream>
24 #include <iomanip>
25 // Suppress warnings about MD5 being deprecated in later versions of OpenSSL
26 #define OPENSSL_SUPPRESS_DEPRECATED 1
27 #include <openssl/md5.h>
28 #include <zlib.h>
29 
30 #include "base64.h"
31 #include "spdlog/spdlog.h"
32 #include "spdlog/fmt/fmt.h"
33 #include "Transmitter.h"
34 #include "File.h"
35 
36 namespace LibFlute {
37 
39  : _meta( std::move(entry) )
40  , _received_at( time(nullptr) )
41  , _file_description()
42 {
43  spdlog::debug("Creating File from FileEntry");
44  // Allocate a data buffer
45  spdlog::debug("Allocating buffer");
46  _buffer = (char*)malloc(_meta.fec_oti.transfer_length);
47  if (_buffer == nullptr)
48  {
49  throw std::runtime_error("Failed to allocate file buffer");
50  }
51  _own_buffer = true;
52 
53  this->calculate_partitioning();
54  this->create_blocks();
55 }
56 
57 File::File(const std::shared_ptr<Transmitter::FileDescription> &file_description)
58  : _meta()
59  , _file_description(file_description)
60 {
61  spdlog::debug("Creating File from FileDescription");
62 
63  auto length = _file_description->data_length();
64  _buffer = (char*)malloc(length);
65  if (_buffer == nullptr)
66  {
67  throw std::runtime_error("No data allocated");
68  }
69  _own_buffer = true;
70  memcpy(_buffer, _file_description->data(), length);
71  _meta = _file_description->file_entry();
72 
73  // for no-code
76  } else {
77  throw std::runtime_error("Unsupported FEC scheme");
78  }
79 
80  encode();
81 
82  calculate_partitioning();
83  create_blocks();
84 }
85 
86 File::File(uint32_t toi,
87  FecOti fec_oti,
88  std::string content_location,
89  std::string content_type,
90  uint64_t expires,
91  char* data,
92  size_t length,
93  bool copy_data)
94  : _own_buffer(false)
95  , _meta()
96  , _file_description()
97 {
98  spdlog::debug("Creating File from data");
99  if (copy_data) {
100  spdlog::debug("Allocating buffer");
101  _buffer = (char*)malloc(length);
102  if (_buffer == nullptr)
103  {
104  throw std::runtime_error("Failed to allocate file buffer");
105  }
106  memcpy(_buffer, data, length);
107  _own_buffer = true;
108  } else {
109  _buffer = data;
110  }
111 
112  unsigned char md5[MD5_DIGEST_LENGTH];
113  MD5((const unsigned char*)data, length, md5);
114 
115  _meta.toi = toi;
116  _meta.content_location = std::move(content_location);
117  _meta.content_type = std::move(content_type);
118  _meta.content_length = length;
119  _meta.content_md5 = base64_encode(md5, MD5_DIGEST_LENGTH);
120  _meta.expires = expires;
121  _meta.fec_oti = fec_oti;
122 
123  // for no-code
126  } else {
127  throw std::runtime_error("Unsupported FEC scheme");
128  }
129 
130  this->calculate_partitioning();
131  this->create_blocks();
132 }
133 
135 {
136  spdlog::debug("Destroying File");
137  if (_own_buffer && _buffer != nullptr)
138  {
139  spdlog::debug("Freeing buffer");
140  free(_buffer);
141  }
142 }
143 
144 auto File::put_symbol( const EncodingSymbol& symbol ) -> void
145 {
146  // Bounds must be ">=", not ">": a valid source-block number is
147  // 0.._source_blocks.size()-1, so SBN == size() is already out of range and
148  // indexing with it below would be undefined behaviour. Throw a std::exception
149  // (not a bare const char*, which escapes the receiver's catch(std::exception&)
150  // and terminates the process) so an out-of-range symbol -- e.g. a content
151  // object whose on-air symbol layout exceeds its FDT-declared FEC-OTI -- is
152  // dropped and logged by the caller instead of crashing the client.
153  if (symbol.source_block_number() >= _source_blocks.size()) {
154  throw std::runtime_error(fmt::format("FLUTE: source block number {} out of range (have {} blocks)",
155  symbol.source_block_number(), _source_blocks.size()));
156  }
157 
158  SourceBlock& source_block = _source_blocks[ symbol.source_block_number() ];
159 
160  if (symbol.id() >= source_block.symbols.size()) {
161  throw std::runtime_error(fmt::format("FLUTE: encoding symbol id {} out of range (block {} has {} symbols)",
162  symbol.id(), symbol.source_block_number(), source_block.symbols.size()));
163  }
164 
165  SourceBlock::Symbol& target_symbol = source_block.symbols[symbol.id()];
166 
167  if (!target_symbol.complete) {
168  symbol.decode_to(target_symbol.data, target_symbol.length);
169  target_symbol.complete = true;
170 
171  check_source_block_completion(source_block);
172  check_file_completion();
173  }
174 
175 }
176 
177 auto File::check_source_block_completion( SourceBlock& block ) -> void
178 {
179  block.complete = std::all_of(block.symbols.begin(), block.symbols.end(), [](const auto& symbol){ return symbol.second.complete; });
180 }
181 
182 auto File::check_file_completion() -> void
183 {
184  _complete = std::all_of(_source_blocks.begin(), _source_blocks.end(), [](const auto& block){ return block.second.complete; });
185 
186  if (_complete && !_meta.content_md5.empty() && _meta.content_encoding.empty()) {
187  //check MD5 sum if we haven't encoded the contents
188  unsigned char md5[MD5_DIGEST_LENGTH];
189  MD5((const unsigned char*)buffer(), length(), md5);
190 
191  auto content_md5 = base64_decode(_meta.content_md5);
192  if (memcmp(md5, content_md5.c_str(), MD5_DIGEST_LENGTH) != 0) {
193  spdlog::debug("MD5 mismatch for TOI {}, discarding", _meta.toi);
194 
195  // MD5 mismatch, try again
196  for (auto& block : _source_blocks) {
197  for (auto& symbol : block.second.symbols) {
198  symbol.second.complete = false;
199  }
200  block.second.complete = false;
201  }
202  _complete = false;
203  }
204  }
205 }
206 
207 auto File::calculate_partitioning() -> void
208 {
209  // Calculate source block partitioning (RFC5052 9.1)
210  _nof_source_symbols = ceil((double)_meta.fec_oti.transfer_length / (double)_meta.fec_oti.encoding_symbol_length);
211  _nof_source_blocks = ceil((double)_nof_source_symbols / (double)_meta.fec_oti.max_source_block_length);
212  _large_source_block_length = ceil((double)_nof_source_symbols / (double)_nof_source_blocks);
213  _small_source_block_length = floor((double)_nof_source_symbols / (double)_nof_source_blocks);
214  _nof_large_source_blocks = _nof_source_symbols - _small_source_block_length * _nof_source_blocks;
215 }
216 
217 auto File::create_blocks() -> void
218 {
219  // Create the required source blocks and encoding symbols
220  auto buffer_ptr = _buffer;
221  size_t remaining_size = _meta.fec_oti.transfer_length;
222  decltype(_nof_large_source_blocks) number = 0;
223  while (remaining_size > 0) {
224  SourceBlock block;
225  size_t symbol_id = 0;
226  auto block_length = ( number < _nof_large_source_blocks ) ? _large_source_block_length : _small_source_block_length;
227 
228  for (decltype(block_length) i = 0; i < block_length; i++) {
229  auto symbol_length = std::min(remaining_size, (size_t)_meta.fec_oti.encoding_symbol_length);
230  assert(buffer_ptr + symbol_length <= _buffer + _meta.fec_oti.transfer_length);
231 
232  SourceBlock::Symbol symbol{.data = buffer_ptr, .length = symbol_length, .complete = false};
233  block.symbols[ symbol_id++ ] = symbol;
234 
235  remaining_size -= symbol_length;
236  buffer_ptr += symbol_length;
237 
238  if (remaining_size <= 0) break;
239  }
240  _source_blocks[number++] = block;
241  }
242 }
243 
244 auto File::get_next_symbols(size_t max_size) -> std::vector<EncodingSymbol>
245 {
246  int nof_symbols = std::ceil((float)(max_size - 4) / (float)_meta.fec_oti.encoding_symbol_length);
247  auto cnt = 0;
248  std::vector<EncodingSymbol> symbols;
249 
250  for (auto& block : _source_blocks) {
251  if (cnt >= nof_symbols) break;
252 
253  if (!block.second.complete) {
254  for (auto& symbol : block.second.symbols) {
255  if (cnt >= nof_symbols) break;
256 
257  if (!symbol.second.complete && !symbol.second.queued) {
258  symbols.emplace_back(symbol.first, block.first, symbol.second.data, symbol.second.length, _meta.fec_oti.encoding_id);
259  symbol.second.queued = true;
260  cnt++;
261  }
262  }
263  }
264  }
265  return symbols;
266 
267 }
268 
269 auto File::mark_completed(const std::vector<EncodingSymbol>& symbols, bool success) -> void
270 {
271  for (auto& symbol : symbols) {
272  auto block = _source_blocks.find(symbol.source_block_number());
273  if (block != _source_blocks.end()) {
274  auto sym = block->second.symbols.find(symbol.id());
275  if (sym != block->second.symbols.end()) {
276  sym->second.queued = false;
277  sym->second.complete = success;
278  }
279  check_source_block_completion(block->second);
280  check_file_completion();
281  }
282  }
283 }
284 
285 auto File::encode() -> void
286 {
287  if (!_been_encoded && !_meta.content_encoding.empty()) {
288  if (_meta.content_encoding == "gzip" || _meta.content_encoding=="deflate") {
289  auto decomp_buffer = _buffer;
290  bool own_decomp = _own_buffer;
291  std::shared_ptr<unsigned char[]> comp_buffer(new unsigned char[16384]);
292  z_stream zs = {
293  .next_in = reinterpret_cast<unsigned char*>(decomp_buffer),
294  .avail_in = static_cast<uint32_t>(_meta.content_length),
295  .next_out = comp_buffer.get(),
296  .avail_out = 16384
297  };
298  spdlog::debug("Compressing contents with {}", _meta.content_encoding);
299 
300  if (deflateInit2(&zs, Z_DEFAULT_COMPRESSION, Z_DEFLATED, 15 | 16, 8, Z_DEFAULT_STRATEGY) == Z_OK) {
301  _buffer = nullptr;
302  auto zstate = deflate(&zs, Z_FINISH);
303  size_t last_out = 0;
304  while (zstate == Z_OK) {
305  spdlog::debug("Part compressed: {} bytes", 16384-zs.avail_out);
306  _buffer = reinterpret_cast<char*>(realloc(_buffer, zs.total_out));
307  memcpy(_buffer+last_out, comp_buffer.get(), 16384-zs.avail_out);
308  last_out = zs.total_out;
309  _own_buffer = true;
310  zs.avail_out = 16384;
311  zs.next_out = comp_buffer.get();
312  zstate = deflate(&zs, Z_FINISH);
313  }
314  if (zstate==Z_STREAM_END) {
315  if (last_out != zs.total_out) {
316  spdlog::debug("Finish compress, last block is {} bytes. Total {} bytes", 16384-zs.avail_out, zs.total_out);
317  _buffer = reinterpret_cast<char*>(realloc(_buffer, zs.total_out));
318  memcpy(_buffer+last_out, comp_buffer.get(), 16384-zs.avail_out);
319  _own_buffer = true;
320  }
321  _meta.fec_oti.transfer_length = zs.total_out;
322  } else {
323  spdlog::error("Error compressing file {}: {}", _meta.toi, zs.msg);
324  throw zs.msg;
325  }
326  deflateEnd(&zs);
327 
328  if (own_decomp) free(decomp_buffer);
329  }
330  } else {
331  spdlog::error("Unknown Content-Encoding {}", _meta.content_encoding);
332  throw std::runtime_error("Content-Encoding not known");
333  }
334 
335  _been_encoded = true;
336  _been_decoded = false;
337  }
338 }
339 
340 auto File::decode() -> void
341 {
342  if (!_been_decoded && !_meta.content_encoding.empty()) {
343  if (_meta.content_encoding == "gzip" || _meta.content_encoding=="deflate") {
344  auto comp_buffer = _buffer;
345  bool own_comp = _own_buffer;
346  std::shared_ptr<unsigned char[]> decomp_buffer(new unsigned char[16384]);
347  z_stream zs = {
348  .next_in = reinterpret_cast<unsigned char*>(comp_buffer),
349  .avail_in = static_cast<uint32_t>(_meta.fec_oti.transfer_length),
350  .next_out = decomp_buffer.get(),
351  .avail_out = 16384
352  };
353  spdlog::debug("Decompressing contents with {}", _meta.content_encoding);
354 
355  inflateInit2(&zs, 15 | ((_meta.content_encoding == "gzip")?16:0));
356  _buffer = nullptr;
357  auto zstate = inflate(&zs, Z_FINISH);
358  size_t last_out = 0;
359  while (zstate == Z_OK) {
360  spdlog::debug("Part decompressed: {} bytes", 16384-zs.avail_out);
361  _buffer = reinterpret_cast<char*>(realloc(_buffer, zs.total_out));
362  memcpy(_buffer+last_out, decomp_buffer.get(), 16384-zs.avail_out);
363  last_out = zs.total_out;
364  _own_buffer = true;
365  zs.avail_out = 16384;
366  zs.next_out = decomp_buffer.get();
367  zstate = inflate(&zs, Z_FINISH);
368  }
369  if (zstate==Z_STREAM_END) {
370  if (last_out != zs.total_out) {
371  spdlog::debug("Finish decompress, last block is {} bytes. Total {} bytes", 16384-zs.avail_out, zs.total_out);
372  _buffer = reinterpret_cast<char*>(realloc(_buffer, zs.total_out));
373  memcpy(_buffer+last_out, decomp_buffer.get(), 16384-zs.avail_out);
374  _own_buffer = true;
375  }
376  if (!_meta.content_length) {
377  _meta.content_length = zs.total_out;
378  } else if (_meta.content_length != zs.total_out) {
379  spdlog::error("Decompressed length does not match expected Content-Length ({} != {})", _meta.content_length, zs.total_out);
380  }
381  } else {
382  spdlog::error("Error decompressing file {}: {}", _meta.toi, zs.msg);
383  throw zs.msg;
384  }
385 
386  if (own_comp) free(comp_buffer);
387  } else {
388  spdlog::error("Unknown Content-Encoding {}", _meta.content_encoding);
389  throw std::runtime_error("Content-Encoding not known");
390  }
391 
392  _been_decoded = true;
393  _been_encoded = false;
394 
395  // Check MD5
396  if (!_meta.content_md5.empty()) {
397  unsigned char md5[MD5_DIGEST_LENGTH];
398  MD5((const unsigned char*)buffer(), length(), md5);
399 
400  auto content_md5 = base64_decode(_meta.content_md5);
401  if (memcmp(md5, content_md5.c_str(), MD5_DIGEST_LENGTH) != 0) {
402  spdlog::debug("MD5 mismatch for TOI {}, discarding", _meta.toi);
403 
404  // MD5 mismatch, try again
405  for (auto& block : _source_blocks) {
406  for (auto& symbol : block.second.symbols) {
407  symbol.second.complete = false;
408  }
409  block.second.complete = false;
410  }
411  _complete = false;
412  }
413  }
414  }
415 }
416 
417 } // end namespace LibFlute
A class for handling FEC encoding symbols.
const FecOti & fec_oti() const
Get the FEC OTI values.
Definition: File.h:112
void put_symbol(const EncodingSymbol &symbol)
Write the data from an encoding symbol into the appropriate place in the buffer.
Definition: File.cpp:144
size_t length() const
Get the data buffer length.
Definition: File.h:90
virtual ~File()
Default destructor.
Definition: File.cpp:134
void encode()
Encode the buffer using the Content-Encoding.
Definition: File.cpp:285
File(LibFlute::FileDeliveryTable::FileEntry entry)
Create a file from an FDT entry (used for reception)
Definition: File.cpp:38
void mark_completed(const std::vector< EncodingSymbol > &symbols, bool success)
Mark encoding symbols as completed.
Definition: File.cpp:269
std::vector< EncodingSymbol > get_next_symbols(size_t max_size)
Get the next encoding symbols that fit in max_size bytes.
Definition: File.cpp:244
void decode()
Decode the buffer using the Content-Encoding.
Definition: File.cpp:340
OTI values struct.
Definition: flute_types.h:51
uint64_t transfer_length
Definition: flute_types.h:54
FecScheme encoding_id
Definition: flute_types.h:52
An entry for a file in the FDT.