libdcp
dcp.cc
Go to the documentation of this file.
1 /*
2  Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
3 
4  This file is part of libdcp.
5 
6  libdcp is free software; you can redistribute it and/or modify
7  it under the terms of the GNU General Public License as published by
8  the Free Software Foundation; either version 2 of the License, or
9  (at your option) any later version.
10 
11  libdcp is distributed in the hope that it will be useful,
12  but WITHOUT ANY WARRANTY; without even the implied warranty of
13  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14  GNU General Public License for more details.
15 
16  You should have received a copy of the GNU General Public License
17  along with libdcp. If not, see <http://www.gnu.org/licenses/>.
18 
19  In addition, as a special exception, the copyright holders give
20  permission to link the code of portions of this program with the
21  OpenSSL library under certain conditions as described in each
22  individual source file, and distribute linked combinations
23  including the two.
24 
25  You must obey the GNU General Public License in all respects
26  for all of the code used other than OpenSSL. If you modify
27  file(s) with this exception, you may extend this exception to your
28  version of the file(s), but you are not obligated to do so. If you
29  do not wish to do so, delete this exception statement from your
30  version. If you delete this exception statement from all source
31  files in the program, then also delete it here.
32 */
33 
34 
40 #include "asset_factory.h"
41 #include "atmos_asset.h"
42 #include "certificate_chain.h"
43 #include "compose.hpp"
44 #include "cpl.h"
45 #include "dcp.h"
46 #include "dcp_assert.h"
47 #include "decrypted_kdm.h"
48 #include "decrypted_kdm_key.h"
49 #include "exceptions.h"
50 #include "font_asset.h"
51 #include "interop_subtitle_asset.h"
52 #include "metadata.h"
53 #include "mono_picture_asset.h"
54 #include "picture_asset.h"
55 #include "pkl.h"
56 #include "raw_convert.h"
57 #include "reel_asset.h"
58 #include "reel_subtitle_asset.h"
59 #include "smpte_subtitle_asset.h"
60 #include "sound_asset.h"
61 #include "stereo_picture_asset.h"
62 #include "util.h"
63 #include "verify.h"
64 #include "warnings.h"
65 LIBDCP_DISABLE_WARNINGS
66 #include <asdcp/AS_DCP.h>
67 LIBDCP_ENABLE_WARNINGS
68 #include <xmlsec/xmldsig.h>
69 #include <xmlsec/app.h>
70 LIBDCP_DISABLE_WARNINGS
71 #include <libxml++/libxml++.h>
72 LIBDCP_ENABLE_WARNINGS
73 #include <boost/filesystem.hpp>
74 #include <boost/algorithm/string.hpp>
75 #include <numeric>
76 
77 
78 using std::string;
79 using std::list;
80 using std::vector;
81 using std::cout;
82 using std::make_pair;
83 using std::map;
84 using std::cerr;
85 using std::make_shared;
86 using std::exception;
87 using std::shared_ptr;
88 using std::dynamic_pointer_cast;
89 using boost::optional;
90 using boost::algorithm::starts_with;
91 using namespace dcp;
92 
93 
94 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
95 static string const assetmap_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
96 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
97 static string const volindex_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
98 
99 
100 DCP::DCP (boost::filesystem::path directory)
101  : _directory (directory)
102 {
103  if (!boost::filesystem::exists (directory)) {
104  boost::filesystem::create_directories (directory);
105  }
106 
107  _directory = boost::filesystem::canonical (_directory);
108 }
109 
110 
111 void
112 DCP::read (vector<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
113 {
114  /* Read the ASSETMAP and PKL */
115 
116  if (boost::filesystem::exists (_directory / "ASSETMAP")) {
117  _asset_map = _directory / "ASSETMAP";
118  } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
119  _asset_map = _directory / "ASSETMAP.xml";
120  } else {
121  boost::throw_exception (MissingAssetmapError(_directory));
122  }
123 
124  cxml::Document asset_map ("AssetMap");
125 
126  asset_map.read_file (_asset_map.get());
127  if (asset_map.namespace_uri() == assetmap_interop_ns) {
128  _standard = Standard::INTEROP;
129  } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
130  _standard = Standard::SMPTE;
131  } else {
132  boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
133  }
134 
135  auto asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
136  map<string, boost::filesystem::path> paths;
137  vector<boost::filesystem::path> pkl_paths;
138  for (auto i: asset_nodes) {
139  if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
140  boost::throw_exception (XMLError ("unsupported asset chunk count"));
141  }
142  auto p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
143  if (starts_with (p, "file://")) {
144  p = p.substr (7);
145  }
146  switch (*_standard) {
147  case Standard::INTEROP:
148  if (i->optional_node_child("PackingList")) {
149  pkl_paths.push_back (p);
150  } else {
151  paths.insert (make_pair(remove_urn_uuid(i->string_child("Id")), p));
152  }
153  break;
154  case Standard::SMPTE:
155  {
156  auto pkl_bool = i->optional_string_child("PackingList");
157  if (pkl_bool && *pkl_bool == "true") {
158  pkl_paths.push_back (p);
159  } else {
160  paths.insert (make_pair(remove_urn_uuid(i->string_child("Id")), p));
161  }
162  break;
163  }
164  }
165  }
166 
167  if (pkl_paths.empty()) {
168  boost::throw_exception (XMLError ("No packing lists found in asset map"));
169  }
170 
171  for (auto i: pkl_paths) {
172  _pkls.push_back (make_shared<PKL>(_directory / i));
173  }
174 
175  /* Now we have:
176  paths - map of files in the DCP that are not PKLs; key is ID, value is path.
177  _pkls - PKL objects for each PKL.
178 
179  Read all the assets from the asset map.
180  */
181 
182  /* Make a list of non-CPL/PKL assets so that we can resolve the references
183  from the CPLs.
184  */
185  vector<shared_ptr<Asset>> other_assets;
186 
187  for (auto i: paths) {
188  auto path = _directory / i.second;
189 
190  if (i.second.empty()) {
191  /* I can't see how this is valid, but it's
192  been seen in the wild with a DCP that
193  claims to come from ClipsterDCI 5.10.0.5.
194  */
195  if (notes) {
196  notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
197  }
198  continue;
199  }
200 
201  if (!boost::filesystem::exists(path)) {
202  if (notes) {
203  notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
204  }
205  continue;
206  }
207 
208  /* Find the <Type> for this asset from the PKL that contains the asset */
209  optional<string> pkl_type;
210  for (auto j: _pkls) {
211  pkl_type = j->type(i.first);
212  if (pkl_type) {
213  break;
214  }
215  }
216 
217  if (!pkl_type) {
218  /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
219  * need to worry about it.
220  */
221  continue;
222  }
223 
224  auto remove_parameters = [](string const& n) {
225  return n.substr(0, n.find(";"));
226  };
227 
228  /* Remove any optional parameters (after ;) */
229  pkl_type = pkl_type->substr(0, pkl_type->find(";"));
230 
231  if (
232  pkl_type == remove_parameters(CPL::static_pkl_type(*_standard)) ||
233  pkl_type == remove_parameters(InteropSubtitleAsset::static_pkl_type(*_standard))) {
234  auto p = new xmlpp::DomParser;
235  try {
236  p->parse_file (path.string());
237  } catch (std::exception& e) {
238  delete p;
239  throw ReadError(String::compose("XML error in %1", path.string()), e.what());
240  }
241 
242  auto const root = p->get_document()->get_root_node()->get_name();
243  delete p;
244 
245  if (root == "CompositionPlaylist") {
246  auto cpl = make_shared<CPL>(path);
247  if (_standard && cpl->standard() != _standard.get() && notes) {
248  notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
249  }
250  _cpls.push_back (cpl);
251  } else if (root == "DCSubtitle") {
252  if (_standard && _standard.get() == Standard::SMPTE && notes) {
253  notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
254  }
255  other_assets.push_back (make_shared<InteropSubtitleAsset>(path));
256  }
257  } else if (
258  *pkl_type == remove_parameters(PictureAsset::static_pkl_type(*_standard)) ||
259  *pkl_type == remove_parameters(SoundAsset::static_pkl_type(*_standard)) ||
260  *pkl_type == remove_parameters(AtmosAsset::static_pkl_type(*_standard)) ||
261  *pkl_type == remove_parameters(SMPTESubtitleAsset::static_pkl_type(*_standard))
262  ) {
263 
264  bool found_threed_marked_as_twod = false;
265  other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type, &found_threed_marked_as_twod));
266  if (found_threed_marked_as_twod && notes) {
267  notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, path});
268  }
269  } else if (*pkl_type == remove_parameters(FontAsset::static_pkl_type(*_standard))) {
270  other_assets.push_back (make_shared<FontAsset>(i.first, path));
271  } else if (*pkl_type == "image/png") {
272  /* It's an Interop PNG subtitle; let it go */
273  } else {
274  throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
275  }
276  }
277 
278  resolve_refs (other_assets);
279 
280  /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
281  if (notes) {
282  for (auto i: cpls()) {
283  for (auto j: i->reel_file_assets()) {
284  if (!j->asset_ref().resolved() && paths.find(j->asset_ref().id()) == paths.end()) {
285  notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
286  }
287  }
288  }
289  }
290 }
291 
292 
293 void
294 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
295 {
296  for (auto i: cpls()) {
297  i->resolve_refs (assets);
298  }
299 }
300 
301 
302 bool
303 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
304 {
305  auto a = cpls ();
306  auto b = other.cpls ();
307 
308  if (a.size() != b.size()) {
309  note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
310  return false;
311  }
312 
313  bool r = true;
314 
315  for (auto i: a) {
316  auto j = b.begin();
317  while (j != b.end() && !(*j)->equals (i, opt, note)) {
318  ++j;
319  }
320 
321  if (j == b.end ()) {
322  r = false;
323  }
324  }
325 
326  return r;
327 }
328 
329 
330 void
331 DCP::add (shared_ptr<CPL> cpl)
332 {
333  _cpls.push_back (cpl);
334 }
335 
336 
337 bool
338 DCP::any_encrypted () const
339 {
340  for (auto i: cpls()) {
341  if (i->any_encrypted()) {
342  return true;
343  }
344  }
345 
346  return false;
347 }
348 
349 
350 bool
351 DCP::all_encrypted () const
352 {
353  for (auto i: cpls()) {
354  if (!i->all_encrypted()) {
355  return false;
356  }
357  }
358 
359  return true;
360 }
361 
362 
363 void
364 DCP::add (DecryptedKDM const & kdm)
365 {
366  auto keys = kdm.keys ();
367 
368  for (auto i: cpls()) {
369  for (auto const& j: kdm.keys()) {
370  if (j.cpl_id() == i->id()) {
371  i->add (kdm);
372  }
373  }
374  }
375 }
376 
377 
381 void
382 DCP::write_volindex (Standard standard) const
383 {
384  auto p = _directory;
385  switch (standard) {
386  case Standard::INTEROP:
387  p /= "VOLINDEX";
388  break;
389  case Standard::SMPTE:
390  p /= "VOLINDEX.xml";
391  break;
392  default:
393  DCP_ASSERT (false);
394  }
395 
396  xmlpp::Document doc;
397  xmlpp::Element* root;
398 
399  switch (standard) {
400  case Standard::INTEROP:
401  root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
402  break;
403  case Standard::SMPTE:
404  root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
405  break;
406  default:
407  DCP_ASSERT (false);
408  }
409 
410  root->add_child("Index")->add_child_text ("1");
411  doc.write_to_file_formatted (p.string (), "UTF-8");
412 }
413 
414 
415 void
417  Standard standard, string pkl_uuid, boost::filesystem::path pkl_path,
418  string issuer, string creator, string issue_date, string annotation_text
419  ) const
420 {
421  auto p = _directory;
422 
423  switch (standard) {
424  case Standard::INTEROP:
425  p /= "ASSETMAP";
426  break;
427  case Standard::SMPTE:
428  p /= "ASSETMAP.xml";
429  break;
430  default:
431  DCP_ASSERT (false);
432  }
433 
434  xmlpp::Document doc;
435  xmlpp::Element* root;
436 
437  switch (standard) {
438  case Standard::INTEROP:
439  root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
440  break;
441  case Standard::SMPTE:
442  root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
443  break;
444  default:
445  DCP_ASSERT (false);
446  }
447 
448  root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
449  root->add_child("AnnotationText")->add_child_text (annotation_text);
450 
451  switch (standard) {
452  case Standard::INTEROP:
453  root->add_child("VolumeCount")->add_child_text ("1");
454  root->add_child("IssueDate")->add_child_text (issue_date);
455  root->add_child("Issuer")->add_child_text (issuer);
456  root->add_child("Creator")->add_child_text (creator);
457  break;
458  case Standard::SMPTE:
459  root->add_child("Creator")->add_child_text (creator);
460  root->add_child("VolumeCount")->add_child_text ("1");
461  root->add_child("IssueDate")->add_child_text (issue_date);
462  root->add_child("Issuer")->add_child_text (issuer);
463  break;
464  default:
465  DCP_ASSERT (false);
466  }
467 
468  auto asset_list = root->add_child ("AssetList");
469 
470  auto asset = asset_list->add_child ("Asset");
471  asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
472  asset->add_child("PackingList")->add_child_text ("true");
473  auto chunk_list = asset->add_child ("ChunkList");
474  auto chunk = chunk_list->add_child ("Chunk");
475  chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
476  chunk->add_child("VolumeIndex")->add_child_text ("1");
477  chunk->add_child("Offset")->add_child_text ("0");
478  chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
479 
480  for (auto i: assets()) {
481  i->write_to_assetmap (asset_list, _directory);
482  }
483 
484  doc.write_to_file_formatted (p.string (), "UTF-8");
485  _asset_map = p;
486 }
487 
488 
489 void
491  string issuer,
492  string creator,
493  string issue_date,
494  string annotation_text,
495  shared_ptr<const CertificateChain> signer,
496  NameFormat name_format
497  )
498 {
499  if (_cpls.empty()) {
500  throw MiscError ("Cannot write DCP with no CPLs.");
501  }
502 
503  auto standard = std::accumulate (
504  std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
505  [](Standard s, shared_ptr<CPL> c) {
506  if (s != c->standard()) {
507  throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
508  }
509  return s;
510  }
511  );
512 
513  for (auto i: cpls()) {
514  NameFormat::Map values;
515  values['t'] = "cpl";
516  i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer);
517  }
518 
519  shared_ptr<PKL> pkl;
520 
521  if (_pkls.empty()) {
522  pkl = make_shared<PKL>(standard, annotation_text, issue_date, issuer, creator);
523  _pkls.push_back (pkl);
524  for (auto i: assets()) {
525  i->add_to_pkl (pkl, _directory);
526  }
527  } else {
528  pkl = _pkls.front ();
529  }
530 
531  NameFormat::Map values;
532  values['t'] = "pkl";
533  auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
534  pkl->write (pkl_path, signer);
535 
536  write_volindex (standard);
537  write_assetmap (standard, pkl->id(), pkl_path, issuer, creator, issue_date, annotation_text);
538 }
539 
540 
541 vector<shared_ptr<CPL>>
542 DCP::cpls () const
543 {
544  return _cpls;
545 }
546 
547 
548 vector<shared_ptr<Asset>>
549 DCP::assets (bool ignore_unresolved) const
550 {
551  vector<shared_ptr<Asset>> assets;
552  for (auto i: cpls()) {
553  assets.push_back (i);
554  for (auto j: i->reel_file_assets()) {
555  if (ignore_unresolved && !j->asset_ref().resolved()) {
556  continue;
557  }
558 
559  auto const id = j->asset_ref().id();
560  auto already_got = false;
561  for (auto k: assets) {
562  if (k->id() == id) {
563  already_got = true;
564  }
565  }
566 
567  if (!already_got) {
568  auto o = j->asset_ref().asset();
569  assets.push_back (o);
570  /* More Interop special-casing */
571  auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
572  if (sub) {
573  sub->add_font_assets (assets);
574  }
575  }
576  }
577  }
578 
579  return assets;
580 }
581 
582 
584 vector<boost::filesystem::path>
585 DCP::directories_from_files (vector<boost::filesystem::path> files)
586 {
587  vector<boost::filesystem::path> d;
588  for (auto i: files) {
589  if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
590  d.push_back (i.parent_path ());
591  }
592  }
593  return d;
594 }
asset_factory() method
AtmosAsset class.
CertificateChain class.
A class to create or read a DCP.
Definition: dcp.h:83
DCP(boost::filesystem::path directory)
Definition: dcp.cc:100
void write_xml(std::string issuer=String::compose("libdcp %1", dcp::version), std::string creator=String::compose("libdcp %1", dcp::version), std::string issue_date=LocalTime().as_string(), std::string annotation_text=String::compose("Created by libdcp %1", dcp::version), std::shared_ptr< const CertificateChain > signer=std::shared_ptr< const CertificateChain >(), NameFormat name_format=NameFormat("%t"))
Definition: dcp.cc:490
bool equals(DCP const &other, EqualityOptions options, NoteHandler note) const
Definition: dcp.cc:303
static std::vector< boost::filesystem::path > directories_from_files(std::vector< boost::filesystem::path > files)
Definition: dcp.cc:585
std::vector< std::shared_ptr< Asset > > assets(bool ignore_unresolved=false) const
Definition: dcp.cc:549
std::vector< std::shared_ptr< PKL > > _pkls
Definition: dcp.h:204
boost::filesystem::path _directory
Definition: dcp.h:200
void write_assetmap(Standard standard, std::string pkl_uuid, boost::filesystem::path pkl_path, std::string issuer, std::string creator, std::string issue_date, std::string annotation_text) const
Definition: dcp.cc:416
std::vector< std::shared_ptr< CPL > > _cpls
Definition: dcp.h:202
boost::optional< Standard > standard() const
Definition: dcp.h:165
void read(std::vector< VerificationNote > *notes=nullptr, bool ignore_incorrect_picture_mxf_type=false)
Definition: dcp.cc:112
void write_volindex(Standard standard) const
Definition: dcp.cc:382
boost::optional< boost::filesystem::path > _asset_map
Definition: dcp.h:206
boost::optional< Standard > _standard
Definition: dcp.h:209
A decrypted KDM.
Definition: decrypted_kdm.h:76
std::vector< DecryptedKDMKey > keys() const
A miscellaneous exception.
Definition: exceptions.h:94
Thrown when no ASSETMAP was found when trying to read a DCP.
Definition: exceptions.h:154
Any error that occurs when reading data from a DCP.
Definition: exceptions.h:106
An XML error.
Definition: exceptions.h:164
CPL class.
DCP class.
DCP_ASSERT macro.
DecryptedKDM class.
DecryptedKDMKey class.
Exceptions thrown by libdcp.
FontAsset class.
InteropSubtitleAsset class.
MXFMetadata class.
Namespace for everything in libdcp.
Definition: array_data.h:50
std::shared_ptr< Asset > asset_factory(boost::filesystem::path path, bool ignore_incorrect_picture_mxf_type, bool *found_threed_marked_as_twod=nullptr)
PictureAsset class.
Methods for conversion to/from string.
ReelAsset class.
ReelSubtitleAsset class.
SMPTESubtitleAsset class.
SoundAsset class.
StereoPictureAsset class.
A class to describe what "equality" means for a particular test.
Definition: types.h:249
Utility methods and classes.
dcp::verify() method and associated code