libflute
FileDeliveryTable.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 // 2025 British Broadcasting Corporation (David Waring <david.waring2@bbc.co.uk>)
5 //
6 // Licensed under the License terms and conditions for use, reproduction, and
7 // distribution of 5G-MAG software (the “License”). You may not use this file
8 // except in compliance with the License. You may obtain a copy of the License at
9 // https://www.5g-mag.com/reference-tools. Unless required by applicable law or
10 // agreed to in writing, software distributed under the License is distributed on
11 // an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
12 // or implied.
13 //
14 // See the License for the specific language governing permissions and limitations
15 // under the License.
16 //
17 #include <stdexcept>
18 #include "FileDeliveryTable.h"
19 #include "tinyxml2.h"
20 #include <iostream>
21 #include <string>
22 #include <map>
23 #include "spdlog/spdlog.h"
24 
25 namespace {
26  class XMLNamespaces {
27  public:
28  XMLNamespaces() :_default_ns() ,_prefix_to_ns_map() {};
29  XMLNamespaces(const XMLNamespaces &to_copy) :_default_ns(to_copy._default_ns) ,_prefix_to_ns_map(to_copy._prefix_to_ns_map) {};
30  XMLNamespaces(XMLNamespaces &&to_move) :_default_ns(std::move(to_move._default_ns)) ,_prefix_to_ns_map(std::move(to_move._prefix_to_ns_map)) {};
31  XMLNamespaces(const tinyxml2::XMLElement *element, const XMLNamespaces &parent_ns = XMLNamespaces());
32 
33  const tinyxml2::XMLElement *findChildElement(const tinyxml2::XMLElement *element, const std::string &name, const std::string &ns = std::string()) const;
34  const tinyxml2::XMLElement *findSiblingElement(const tinyxml2::XMLElement *child_elem, const std::string &name, const std::string &ns = std::string()) const;
35  const tinyxml2::XMLAttribute *findAttribute(const tinyxml2::XMLElement *element, const std::string &name, const std::string &ns = std::string()) const;
36  bool matches(const std::string &prefixed_name, const std::string &name, const std::string &ns = std::string()) const;
37  const std::string &elementNamespace(const tinyxml2::XMLElement *element) const;
38 
39  private:
40  std::string _default_ns; // namespace given for "xmlns=..."
41  std::map<std::string, std::string> _prefix_to_ns_map; // namespaces given for "xmlns:prefix=..."
42  };
43 
44  XMLNamespaces::XMLNamespaces(const tinyxml2::XMLElement *element, const XMLNamespaces &parent_ns)
45  :_default_ns(parent_ns._default_ns)
46  ,_prefix_to_ns_map(parent_ns._prefix_to_ns_map)
47  {
48  if (!element) return;
49  for (auto attr_ptr = element->FirstAttribute(); attr_ptr; attr_ptr = attr_ptr->Next()) {
50  std::string attr_name = attr_ptr->Name();
51  if (attr_name == "xmlns") {
52  _default_ns = std::string(attr_ptr->Value());
53  } else if (attr_name.substr(0,6) == "xmlns:") {
54  _prefix_to_ns_map.insert(std::make_pair(attr_name.substr(6), std::string(attr_ptr->Value())));
55  }
56  }
57  }
58 
59  const tinyxml2::XMLElement *XMLNamespaces::findChildElement(const tinyxml2::XMLElement *element, const std::string &name, const std::string &ns) const
60  {
61  if (!element) return nullptr;
62  auto elem_ptr = element->FirstChildElement();
63  if (!elem_ptr) return nullptr;
64  XMLNamespaces child_ns(elem_ptr, *this);
65  if (child_ns.matches(elem_ptr->Name(), name, ns)) return elem_ptr;
66  return findSiblingElement(elem_ptr, name, ns);
67  }
68 
69  const tinyxml2::XMLElement *XMLNamespaces::findSiblingElement(const tinyxml2::XMLElement *child_elem, const std::string &name, const std::string &ns) const
70  {
71  if (!child_elem) return nullptr;
72  for (auto elem_ptr = child_elem->NextSiblingElement(); elem_ptr; elem_ptr = elem_ptr->NextSiblingElement()) {
73  XMLNamespaces child_ns(elem_ptr, *this);
74  if (child_ns.matches(elem_ptr->Name(), name, ns)) return elem_ptr;
75  }
76  return nullptr;
77  }
78 
79  const tinyxml2::XMLAttribute *XMLNamespaces::findAttribute(const tinyxml2::XMLElement *element, const std::string &name, const std::string &ns) const
80  {
81  const std::string &elem_ns = elementNamespace(element);
82  for (auto attr_ptr = element->FirstAttribute(); attr_ptr; attr_ptr = attr_ptr->Next()) {
83  XMLNamespaces attr_ns(*this);
84  attr_ns._default_ns = elem_ns;
85  if (attr_ns.matches(attr_ptr->Name(), name, ns)) return attr_ptr;
86  }
87  return nullptr;
88  }
89 
90  bool XMLNamespaces::matches(const std::string &prefixed_name, const std::string &name, const std::string &ns) const
91  {
92  std::string match_ns(_default_ns);
93  std::string match_name(prefixed_name);
94  auto pos = match_name.find_first_of(':');
95  if (pos != std::string::npos) {
96  auto prefix = match_name.substr(0,pos);
97  match_name.erase(0,pos+1);
98  auto it = _prefix_to_ns_map.find(prefix);
99  if (it != _prefix_to_ns_map.end()) {
100  match_ns = _prefix_to_ns_map.at(prefix);
101  } else {
102  match_ns = prefix;
103  }
104  }
105  return name == match_name && ns == match_ns;
106  }
107 
108  const std::string &XMLNamespaces::elementNamespace(const tinyxml2::XMLElement *element) const
109  {
110  std::string elem_name(element->Name());
111  auto pos = elem_name.find_first_of(':');
112  if (pos != std::string::npos) {
113  auto elem_prefix = elem_name.substr(0,pos);
114  auto it = _prefix_to_ns_map.find(elem_prefix);
115  if (it != _prefix_to_ns_map.end()) {
116  return _prefix_to_ns_map.at(elem_prefix);
117  } else {
118  static const std::string unknown("<Unknown>");
119  return unknown;
120  }
121  }
122  return _default_ns;
123  }
124 }
125 
127 {
128  return toi == other.toi && content_length == other.content_length && expires == other.expires &&
129  cache_control.no_cache == other.cache_control.no_cache && fec_oti == other.fec_oti &&
130  cache_control.cache_expires == other.cache_control.cache_expires && content_location == other.content_location &&
132  etag == other.etag;
133 }
134 
136  : _instance_id( instance_id )
137  , _instance_id_sent( instance_id - 1 )
138  , _global_fec_oti( fec_oti )
139  , _fdt_namespace( fdt_namespace )
140 {
141 }
142 
143 LibFlute::FileDeliveryTable::FileDeliveryTable(uint32_t instance_id, char* buffer, size_t len)
144  : _instance_id( instance_id )
145  , _instance_id_sent( instance_id - 1 )
146  , _global_fec_oti()
147 {
148  static const std::string mbms2007_ns("urn:3GPP:metadata:2007:MBMS:FLUTE:FDT"); // 3GPP TS 26.346 Clause 7.2.10.2
149  static const std::string mbms2012_ns("urn:3GPP:metadata:2012:MBMS:FLUTE:FDT"); // 3GPP TS 26.346 Clause 7.2.10.2
150  tinyxml2::XMLDocument doc(true, tinyxml2::COLLAPSE_WHITESPACE);
151  doc.Parse(buffer, len);
152  auto fdt_instance = doc.RootElement();
153  // A malformed / truncated FDT buffer leaves no root element; every access
154  // below (elementNamespace, Name(), ...) would then dereference null and crash
155  // the receiver. Throw instead so Receiver::handle_receive_from's
156  // catch(std::exception&) drops the bad FDT and keeps the client alive.
157  if (doc.Error() || fdt_instance == nullptr) {
158  spdlog::warn("FDT parse failed ({}) at line {}, len={}",
159  doc.ErrorName() ? doc.ErrorName() : "?", doc.ErrorLineNum(), len);
160  throw std::runtime_error(std::string("FDT XML parse failed: ") +
161  (doc.ErrorName() ? doc.ErrorName() : "no root element"));
162  }
163  XMLNamespaces root_ns(fdt_instance);
164  auto fdt_ns = root_ns.elementNamespace(fdt_instance);
165  if (!root_ns.matches(fdt_instance->Name(), "FDT-Instance", fdt_ns)) {
166  throw std::runtime_error("Root element is not FDT-Instance");
167  }
168 
169  if (fdt_ns == "") {
170  _fdt_namespace = FDT_NS_NONE;
171  } else if (fdt_ns == "http://www.example.com/flute") { // RFC 3926 Section 3.4.2
172  _fdt_namespace = FDT_NS_RFC3926;
173  } else if (fdt_ns == "urn:IETF:metadata:2005:FLUTE:FDT") { // 3GPP TS 26.346 Clause 7.2.10.1
174  _fdt_namespace = FDT_NS_DRAFT_2005;
175 // } else if (fdt_ns == "urn:ietf:params:xml:ns:fdt") { // RFC 6726 - FLUTEv2 - needs more work
176 // _fdt_namespace = FDT_NS_RFC6726;
177  } else if (fdt_ns == "urn:3GPP:metadata:2022:FLUTE:FDT") { // 3GPP TS 26.346 Clause L.6.1
178  _fdt_namespace = FDT_NS_3GPP_CONSOLIDATED_V2;
179  } else {
180  throw std::runtime_error("FDT namespace not recognised");
181  }
182 
183  _expires = std::stoull(root_ns.findAttribute(fdt_instance, "Expires", fdt_ns)->Value());
184 
185  spdlog::debug("Received new FDT with instance ID {}: {}", instance_id, buffer);
186 
187  auto val = root_ns.findAttribute(fdt_instance, "FEC-OTI-FEC-Encoding-ID", fdt_ns);
188  if (val != nullptr) {
189  _global_fec_oti.encoding_id = static_cast<FecScheme>(strtoul(val->Value(), nullptr, 0));
190  }
191 
192  val = root_ns.findAttribute(fdt_instance, "FEC-OTI-FEC-Instance-ID", fdt_ns);
193  if (val != nullptr) {
194  _global_fec_oti.instance_id = strtoul(val->Value(), nullptr, 0);
195  }
196 
197  val = root_ns.findAttribute(fdt_instance, "FEC-OTI-Maximum-Source-Block-Length", fdt_ns);
198  if (val != nullptr) {
199  _global_fec_oti.max_source_block_length = strtoul(val->Value(), nullptr, 0);
200  }
201 
202  val = root_ns.findAttribute(fdt_instance, "FEC-OTI-Encoding-Symbol-Length", fdt_ns);
203  if (val != nullptr) {
204  _global_fec_oti.encoding_symbol_length = strtoul(val->Value(), nullptr, 0);
205  }
206 
207  val = root_ns.findAttribute(fdt_instance, "FEC-OTI-Max-Number-of-Encoding-Symbols", fdt_ns);
208  if (val != nullptr) {
209  _global_fec_oti.max_number_of_encoding_symbols = strtoul(val->Value(), nullptr, 0);
210  }
211 
212  for (auto file = root_ns.findChildElement(fdt_instance, "File", fdt_ns);
213  file != nullptr; file = root_ns.findSiblingElement(file, "File", fdt_ns)) {
214 
215  XMLNamespaces file_ns(file, root_ns);
216 
217  // File required attributes
218  auto toi_str = file_ns.findAttribute(file, "TOI", fdt_ns);
219  if (toi_str == nullptr) {
220  throw std::runtime_error("Missing TOI attribute on File element");
221  }
222  uint32_t toi = strtoull(toi_str->Value(), nullptr, 0);
223 
224  auto content_location = file_ns.findAttribute(file, "Content-Location", fdt_ns);
225  if (content_location == nullptr) {
226  throw std::runtime_error("Missing Content-Location attribute on File element");
227  }
228 
229  // File optional attributes
230  uint32_t content_length = 0;
231  val = file_ns.findAttribute(file, "Content-Length", fdt_ns);
232  if (val != nullptr) {
233  content_length = strtoull(val->Value(), nullptr, 0);
234  }
235 
236  uint32_t transfer_length = 0;
237  val = file_ns.findAttribute(file, "Transfer-Length", fdt_ns);
238  if (val != nullptr) {
239  transfer_length = strtoull(val->Value(), nullptr, 0);
240  } else {
241  transfer_length = content_length;
242  }
243 
244  auto content_md5 = std::string();
245  val = file_ns.findAttribute(file, "Content-MD5", fdt_ns);
246  if (val != nullptr) {
247  content_md5 = val->Value();
248  }
249 
250  auto content_encoding = std::string();
251  val = file_ns.findAttribute(file, "Content-Encoding", fdt_ns);
252  if (val != nullptr) {
253  content_encoding = val->Value();
254  }
255 
256  auto content_type = std::string();
257  val = file_ns.findAttribute(file, "Content-Type", fdt_ns);
258  if (val != nullptr) {
259  content_type = val->Value();
260  }
261 
262  auto encoding_id = _global_fec_oti.encoding_id;
263  val = file_ns.findAttribute(file, "FEC-OTI-FEC-Encoding-ID", fdt_ns);
264  if (val != nullptr) {
265  encoding_id = static_cast<FecScheme>(strtoul(val->Value(), nullptr, 0));
266  }
267 
268  auto fec_instance_id = _global_fec_oti.instance_id;
269  val = file_ns.findAttribute(file, "FEC-OTI-FEC-Instance-ID", fdt_ns);
270  if (val != nullptr) {
271  fec_instance_id = strtoul(val->Value(), nullptr, 0);
272  }
273 
274  auto max_source_block_length = _global_fec_oti.max_source_block_length;
275  val = file_ns.findAttribute(file, "FEC-OTI-Maximum-Source-Block-Length", fdt_ns);
276  if (val != nullptr) {
277  max_source_block_length = strtoul(val->Value(), nullptr, 0);
278  }
279 
280  auto encoding_symbol_length = _global_fec_oti.encoding_symbol_length;
281  val = file_ns.findAttribute(file, "FEC-OTI-Encoding-Symbol-Length", fdt_ns);
282  if (val != nullptr) {
283  encoding_symbol_length = strtoul(val->Value(), nullptr, 0);
284  }
285 
286  auto max_number_of_encoding_symbols = _global_fec_oti.max_number_of_encoding_symbols;
287  val = file_ns.findAttribute(file, "FEC-OTI-Max-Number-of-Encoding-Symbols", fdt_ns);
288  if (val != nullptr) {
289  max_number_of_encoding_symbols = strtoul(val->Value(), nullptr, 0);
290  }
291 
292  auto mbms2012_file_etag = "";
293  val = file_ns.findAttribute(file, "File-ETag", mbms2012_ns);
294  if (val != nullptr) {
295  mbms2012_file_etag = val->Value();
296  }
297 
298  // File optional elements
299 
300  bool no_cache = false;
301  //bool max_stale = false;
302  std::optional<uint64_t> cache_expires = std::nullopt;
303  auto cc = file_ns.findChildElement(file, "Cache-Control", mbms2007_ns);
304  if (cc) {
305  XMLNamespaces cc_ns(cc, file_ns);
306 
307  // mbms2007:Cache-Control optional elements
308 
309  auto no_cache_elem = cc_ns.findChildElement(cc, "no-cache", mbms2007_ns);
310  if (no_cache_elem) {
311  no_cache = true;
312  }
313 
314  //auto max_stale_elem = cc_ns.findChildElement(cc, "max-stale", mbms2007_ns);
315  //if (max_stale_elem) {
316  // max_stale = true;
317  //}
318 
319  auto expires_elem = cc_ns.findChildElement(cc, "Expires", mbms2007_ns);
320  if (expires_elem) {
321  cache_expires = strtoul(expires_elem->GetText(), nullptr, 0);
322  }
323  }
324 
325  FecOti fec_oti{
326  .encoding_id = (FecScheme)encoding_id,
327  .instance_id = fec_instance_id,
328  .transfer_length = transfer_length,
329  .encoding_symbol_length = encoding_symbol_length,
330  .max_source_block_length = max_source_block_length,
331  .max_number_of_encoding_symbols = max_number_of_encoding_symbols
332  };
333 
334  FileEntry fe{
335  toi,
336  std::string(content_location->Value()),
337  content_length,
338  content_md5,
339  content_type,
340  (cache_expires)?(cache_expires.value()):0,
341  fec_oti,
342  {
343  no_cache,
344  cache_expires
345  },
346  content_encoding,
347  mbms2012_file_etag
348  };
349  _file_entries.push_back(fe);
350  }
351 }
352 
354 {
355  if (_instance_id == _instance_id_sent) _instance_id++;
356  _file_entries.push_back(fe);
357 }
358 
359 auto LibFlute::FileDeliveryTable::remove(uint32_t toi) -> void
360 {
361  for (auto it = _file_entries.cbegin(); it != _file_entries.cend();) {
362  if (it->toi == toi) {
363  it = _file_entries.erase(it);
364  } else {
365  ++it;
366  }
367  }
368  if (_instance_id == _instance_id_sent) _instance_id++;
369 }
370 
371 auto LibFlute::FileDeliveryTable::to_string() const -> std::string {
372  tinyxml2::XMLDocument doc;
373  doc.InsertFirstChild( doc.NewDeclaration() );
374  auto root = doc.NewElement("FDT-Instance");
375  switch (_fdt_namespace) {
376  case FDT_NS_RFC3926:
377  // RFC 3926 Section 3.4.2
378  root->SetAttribute("xmlns", "http://www.example.com/flute");
379  break;
380  case FDT_NS_DRAFT_2005:
381  // 3GPP TS 26.346 Clause 7.2.10.1
382  root->SetAttribute("xmlns", "urn:IETF:metadata:2005:FLUTE:FDT");
383  break;
384 // case FDT_NS_RFC6726: // FLUTE v2 - Will need other things implementing to use this
385 // // RFC 6726
386 // root->SetAttribute("xmlns", "urn:ietf:params:xml:ns:fdt");
387 // break;
388  case FDT_NS_3GPP_CONSOLIDATED_V2:
389  // 3GPP TS 26.346 Clause L.6.1
390  root->SetAttribute("xmlns", "urn:3GPP:metadata:2022:FLUTE:FDT");
391  break;
392  default:
393  break;
394  }
395  root->SetAttribute("Expires", std::to_string(_expires).c_str());
396  root->SetAttribute("FEC-OTI-FEC-Encoding-ID", (unsigned)_global_fec_oti.encoding_id);
397  if (_global_fec_oti.instance_id) root->SetAttribute("FEC-OTI-FEC-Instance-ID", (unsigned)_global_fec_oti.instance_id);
398  root->SetAttribute("FEC-OTI-Maximum-Source-Block-Length", (unsigned)_global_fec_oti.max_source_block_length);
399  root->SetAttribute("FEC-OTI-Encoding-Symbol-Length", (unsigned)_global_fec_oti.encoding_symbol_length);
400  root->SetAttribute("xmlns:mbms2007", "urn:3GPP:metadata:2007:MBMS:FLUTE:FDT"); // 3GPP TS 26.346 Clause 7.2.10.2
401  root->SetAttribute("xmlns:mbms2012", "urn:3GPP:metadata:2012:MBMS:FLUTE:FDT"); // 3GPP TS 26.346 Clause 7.2.10.2
402  doc.InsertEndChild(root);
403 
404  for (const auto& file : _file_entries) {
405  auto f = doc.NewElement("File");
406  f->SetAttribute("TOI", file.toi);
407  f->SetAttribute("Content-Location", file.content_location.c_str());
408  f->SetAttribute("Content-Length", file.content_length);
409  if (file.fec_oti.transfer_length) f->SetAttribute("Transfer-Length", file.fec_oti.transfer_length);
410  if (!file.content_md5.empty()) f->SetAttribute("Content-MD5", file.content_md5.c_str());
411  if (!file.content_encoding.empty()) f->SetAttribute("Content-Encoding", file.content_encoding.c_str());
412  if (!file.content_type.empty()) f->SetAttribute("Content-Type", file.content_type.c_str());
413  if (file.fec_oti.encoding_id != _global_fec_oti.encoding_id)
414  f->SetAttribute("FEC-OTI-FEC-Encoding-ID", (unsigned)file.fec_oti.encoding_id);
415  if (file.fec_oti.instance_id != 0 && file.fec_oti.instance_id != _global_fec_oti.instance_id)
416  f->SetAttribute("FEC-OTI-FEC-Instance-ID", (unsigned)file.fec_oti.instance_id);
417  if (file.fec_oti.max_source_block_length != 0 &&
418  file.fec_oti.max_source_block_length != _global_fec_oti.max_source_block_length)
419  f->SetAttribute("FEC-OTI-Maximum-Source-Block-Length", (unsigned)file.fec_oti.max_source_block_length);
420  if (file.fec_oti.encoding_symbol_length != 0 &&
421  file.fec_oti.encoding_symbol_length != _global_fec_oti.encoding_symbol_length)
422  f->SetAttribute("FEC-OTI-Encoding-Symbol-Length", (unsigned)file.fec_oti.encoding_symbol_length);
423  if (!file.etag.empty()) f->SetAttribute("mbms2012:File-ETag", file.etag.c_str());
424  if (file.cache_control.no_cache || file.cache_control.cache_expires) {
425  auto cc = doc.NewElement("mbms2007:Cache-Control");
426  if (file.cache_control.no_cache) {
427  auto noc = doc.NewElement("mbms2007:no-cache");
428  noc->SetText("true");
429  cc->InsertEndChild(noc);
430  } else {
431  auto exp = doc.NewElement("mbms2007:Expires");
432  exp->SetText(std::to_string(file.expires).c_str());
433  cc->InsertEndChild(exp);
434  }
435  f->InsertEndChild(cc);
436  }
437  root->InsertEndChild(f);
438  }
439 
440 
441  tinyxml2::XMLPrinter printer;
442  doc.Print(&printer);
443  return std::string(printer.CStr());
444 }
FdtNamespace
FDT namespace enumeration.
uint32_t instance_id()
Get the FDT instance ID.
void remove(uint32_t toi)
Remove a file entry.
std::string to_string() const
Serialize the FDT to an XML string.
void add(const FileEntry &entry)
Add a file entry.
FileDeliveryTable(uint32_t instance_id, FecOti fec_oti, FdtNamespace fdt_namespace=FDT_NS_NONE)
Create an empty FDT.
static char doc[]
FecScheme
Error correction schemes.
Definition: flute_types.h:44
OTI values struct.
Definition: flute_types.h:51
uint32_t instance_id
Definition: flute_types.h:53
FecScheme encoding_id
Definition: flute_types.h:52
uint32_t max_source_block_length
Definition: flute_types.h:56
uint32_t max_number_of_encoding_symbols
Definition: flute_types.h:57
uint32_t encoding_symbol_length
Definition: flute_types.h:55
An entry for a file in the FDT.
std::optional< uint64_t > cache_expires
struct LibFlute::FileDeliveryTable::FileEntry::@0 cache_control
bool operator==(const FileEntry &other) const