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 "filesystem.h"
51 #include "font_asset.h"
52 #include "interop_text_asset.h"
53 #include "metadata.h"
54 #include "mono_j2k_picture_asset.h"
56 #include "j2k_picture_asset.h"
57 #include "pkl.h"
58 #include "raw_convert.h"
59 #include "reel_asset.h"
60 #include "reel_text_asset.h"
61 #include "smpte_text_asset.h"
62 #include "sound_asset.h"
64 #include "util.h"
65 #include "verify.h"
66 #include "warnings.h"
67 LIBDCP_DISABLE_WARNINGS
68 #include <asdcp/AS_DCP.h>
69 LIBDCP_ENABLE_WARNINGS
70 #include <xmlsec/xmldsig.h>
71 #include <xmlsec/app.h>
72 LIBDCP_DISABLE_WARNINGS
73 #include <libxml++/libxml++.h>
74 LIBDCP_ENABLE_WARNINGS
75 #include <boost/algorithm/string.hpp>
76 #include <numeric>
77 
78 
79 using std::cerr;
80 using std::cout;
81 using std::dynamic_pointer_cast;
82 using std::exception;
83 using std::list;
84 using std::make_pair;
85 using std::make_shared;
86 using std::map;
87 using std::shared_ptr;
88 using std::string;
89 using std::vector;
90 using boost::algorithm::starts_with;
91 using boost::optional;
92 using namespace dcp;
93 
94 
95 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
96 static string const volindex_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
97 
98 
99 DCP::DCP (boost::filesystem::path directory)
100  : _directory (directory)
101 {
102  if (!filesystem::exists(directory)) {
103  filesystem::create_directories(directory);
104  }
105 
106  _directory = filesystem::canonical(_directory);
107 }
108 
109 
110 DCP::DCP(DCP&& other)
111  : _directory(std::move(other._directory))
112  , _cpls(std::move(other._cpls))
113  , _pkls(std::move(other._pkls))
114  , _asset_map(std::move(other._asset_map))
115  , _new_issuer(std::move(other._new_issuer))
116  , _new_creator(std::move(other._new_creator))
117  , _new_issue_date(std::move(other._new_issue_date))
118  , _new_annotation_text(std::move(other._new_annotation_text))
119 {
120 
121 }
122 
123 
124 DCP&
125 DCP::operator=(DCP&& other)
126 {
127  _directory = std::move(other._directory);
128  _cpls = std::move(other._cpls);
129  _pkls = std::move(other._pkls);
130  _asset_map = std::move(other._asset_map);
131  _new_issuer = std::move(other._new_issuer);
132  _new_creator = std::move(other._new_creator);
133  _new_issue_date = std::move(other._new_issue_date);
134  _new_annotation_text = std::move(other._new_annotation_text);
135  return *this;
136 }
137 
138 
139 void
140 DCP::read (vector<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
141 {
142  /* Read the ASSETMAP and PKL */
143 
144  boost::filesystem::path asset_map_path;
145  if (filesystem::exists(_directory / "ASSETMAP")) {
146  asset_map_path = _directory / "ASSETMAP";
147  } else if (filesystem::exists(_directory / "ASSETMAP.xml")) {
148  asset_map_path = _directory / "ASSETMAP.xml";
149  } else {
150  boost::throw_exception(MissingAssetmapError(_directory));
151  }
152 
153  _asset_map = AssetMap(asset_map_path);
154  auto const pkl_paths = _asset_map->pkl_paths();
155  auto const standard = _asset_map->standard();
156 
157  if (pkl_paths.empty()) {
158  boost::throw_exception (XMLError ("No packing lists found in asset map"));
159  }
160 
161  for (auto i: pkl_paths) {
162  _pkls.push_back(make_shared<PKL>(i, notes));
163  }
164 
165  /* Now we have:
166  paths - map of files in the DCP that are not PKLs; key is ID, value is path.
167  _pkls - PKL objects for each PKL.
168 
169  Read all the assets from the asset map.
170  */
171 
172  /* Make a list of non-CPL/PKL assets so that we can resolve the references
173  from the CPLs.
174  */
175  vector<shared_ptr<Asset>> other_assets;
176 
177  auto ids_and_paths = _asset_map->asset_ids_and_paths();
178  for (auto id_and_path: ids_and_paths) {
179  auto const id = id_and_path.first;
180  auto const path = id_and_path.second;
181 
182  if (path == _directory) {
183  /* I can't see how this is valid, but it's
184  been seen in the wild with a DCP that
185  claims to come from ClipsterDCI 5.10.0.5.
186  */
187  if (notes) {
188  notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
189  }
190  continue;
191  }
192 
193  if (!filesystem::exists(path)) {
194  if (notes) {
195  notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
196  }
197  continue;
198  }
199 
200  /* Find the <Type> for this asset from the PKL that contains the asset */
201  optional<string> pkl_type;
202  for (auto j: _pkls) {
203  pkl_type = j->type(id);
204  if (pkl_type) {
205  break;
206  }
207  }
208 
209  if (!pkl_type) {
210  /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
211  * need to worry about it.
212  */
213  continue;
214  }
215 
216  auto remove_parameters = [](string const& n) {
217  return n.substr(0, n.find(";"));
218  };
219 
220  /* Remove any optional parameters (after ;) */
221  pkl_type = pkl_type->substr(0, pkl_type->find(";"));
222 
223  if (
224  pkl_type == remove_parameters(CPL::static_pkl_type(standard)) ||
225  pkl_type == remove_parameters(InteropTextAsset::static_pkl_type(standard))) {
226  auto p = new xmlpp::DomParser;
227  try {
228  p->parse_file(dcp::filesystem::fix_long_path(path).string());
229  } catch (std::exception& e) {
230  delete p;
231  throw ReadError(String::compose("XML error in %1", path.string()), e.what());
232  }
233 
234  auto const root = p->get_document()->get_root_node()->get_name();
235  delete p;
236 
237  if (root == "CompositionPlaylist") {
238  auto cpl = make_shared<CPL>(path, notes);
239  if (cpl->standard() != standard && notes) {
240  notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
241  }
242  _cpls.push_back (cpl);
243  } else if (root == "DCSubtitle") {
244  if (standard == Standard::SMPTE && notes) {
245  notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
246  }
247  other_assets.push_back (make_shared<InteropTextAsset>(path));
248  }
249  } else if (
250  *pkl_type == remove_parameters(J2KPictureAsset::static_pkl_type(standard)) ||
251  *pkl_type == remove_parameters(MPEG2PictureAsset::static_pkl_type(standard)) ||
252  *pkl_type == remove_parameters(SoundAsset::static_pkl_type(standard)) ||
253  *pkl_type == remove_parameters(AtmosAsset::static_pkl_type(standard)) ||
254  *pkl_type == remove_parameters(SMPTETextAsset::static_pkl_type(standard))
255  ) {
256 
257  bool found_threed_marked_as_twod = false;
258  auto asset = asset_factory(path, ignore_incorrect_picture_mxf_type, &found_threed_marked_as_twod);
259  if (asset->id() != id) {
260  notes->push_back(VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_ASSET_MAP_ID).set_id(id).set_other_id(asset->id()));
261  }
262  other_assets.push_back(asset);
263  if (found_threed_marked_as_twod && notes) {
264  notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, path});
265  }
266  } else if (*pkl_type == remove_parameters(FontAsset::static_pkl_type(standard))) {
267  other_assets.push_back(make_shared<FontAsset>(id, path));
268  } else if (*pkl_type == "image/png") {
269  /* It's an Interop PNG subtitle; let it go */
270  } else {
271  throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
272  }
273  }
274 
275  /* Set hashes for assets where we have an idea of what the hash should be in either a CPL or PKL.
276  * This means that when the hash is later read from these objects the result will be the one that
277  * it should be, rather the one that it currently is. This should prevent errors being concealed
278  * when an asset is corrupted - the hash from the CPL/PKL will disagree with the actual hash of the
279  * file, revealing the problem.
280  */
281 
282  auto hash_from_pkl = [this](string id) -> optional<string> {
283  for (auto pkl: _pkls) {
284  if (auto pkl_hash = pkl->hash(id)) {
285  return pkl_hash;
286  }
287  }
288 
289  return {};
290  };
291 
292  auto hash_from_cpl_or_pkl = [this, &hash_from_pkl](string id) -> optional<string> {
293  for (auto cpl: cpls()) {
294  for (auto reel_file_asset: cpl->reel_file_assets()) {
295  if (reel_file_asset->asset_ref().id() == id && reel_file_asset->hash()) {
296  return reel_file_asset->hash();
297  }
298  }
299  }
300 
301  return hash_from_pkl(id);
302  };
303 
304  for (auto asset: other_assets) {
305  if (auto hash = hash_from_cpl_or_pkl(asset->id())) {
306  asset->set_hash(*hash);
307  }
308  }
309 
310  for (auto cpl: cpls()) {
311  if (auto hash = hash_from_pkl(cpl->id())) {
312  cpl->set_hash(*hash);
313  }
314  }
315 
316  /* Resolve references */
317  resolve_refs (other_assets);
318 
319  /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
320  if (notes) {
321  for (auto i: cpls()) {
322  for (auto j: i->reel_file_assets()) {
323  if (!j->asset_ref().resolved() && ids_and_paths.find(j->asset_ref().id()) == ids_and_paths.end()) {
324  notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
325  }
326  }
327  }
328  }
329 }
330 
331 
332 void
333 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
334 {
335  for (auto i: cpls()) {
336  i->resolve_refs (assets);
337  }
338 }
339 
340 
341 bool
342 DCP::equals(DCP const & other, EqualityOptions const& opt, NoteHandler note) const
343 {
344  auto a = cpls ();
345  auto b = other.cpls ();
346 
347  if (a.size() != b.size()) {
348  note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
349  return false;
350  }
351 
352  bool r = true;
353 
354  for (auto i: a) {
355  auto j = b.begin();
356  while (j != b.end() && !(*j)->equals (i, opt, note)) {
357  ++j;
358  }
359 
360  if (j == b.end ()) {
361  r = false;
362  }
363  }
364 
365  return r;
366 }
367 
368 
369 void
370 DCP::add (shared_ptr<CPL> cpl)
371 {
372  _cpls.push_back (cpl);
373 }
374 
375 
376 bool
377 DCP::any_encrypted () const
378 {
379  for (auto i: cpls()) {
380  if (i->any_encrypted()) {
381  return true;
382  }
383  }
384 
385  return false;
386 }
387 
388 
389 bool
390 DCP::all_encrypted () const
391 {
392  for (auto i: cpls()) {
393  if (!i->all_encrypted()) {
394  return false;
395  }
396  }
397 
398  return true;
399 }
400 
401 
402 void
403 DCP::add (DecryptedKDM const & kdm)
404 {
405  auto keys = kdm.keys();
406  for (auto cpl: cpls()) {
407  if (std::any_of(keys.begin(), keys.end(), [cpl](DecryptedKDMKey const& key) { return key.cpl_id() == cpl->id(); })) {
408  cpl->add (kdm);
409  }
410  }
411 }
412 
413 
417 void
418 DCP::write_volindex (Standard standard) const
419 {
420  auto p = _directory;
421  switch (standard) {
422  case Standard::INTEROP:
423  p /= "VOLINDEX";
424  break;
425  case Standard::SMPTE:
426  p /= "VOLINDEX.xml";
427  break;
428  default:
429  DCP_ASSERT (false);
430  }
431 
432  xmlpp::Document doc;
433  xmlpp::Element* root;
434 
435  switch (standard) {
436  case Standard::INTEROP:
437  root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
438  break;
439  case Standard::SMPTE:
440  root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
441  break;
442  default:
443  DCP_ASSERT (false);
444  }
445 
446  cxml::add_text_child(root, "Index", "1");
447  doc.write_to_file_formatted(dcp::filesystem::fix_long_path(p).string(), "UTF-8");
448 }
449 
450 
451 void
452 DCP::write_xml(shared_ptr<const CertificateChain> signer, bool include_mca_subdescriptors, NameFormat name_format)
453 {
454  if (_cpls.empty()) {
455  throw MiscError ("Cannot write DCP with no CPLs.");
456  }
457 
458  auto standard = std::accumulate (
459  std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
460  [](Standard s, shared_ptr<CPL> c) {
461  if (s != c->standard()) {
462  throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
463  }
464  return s;
465  }
466  );
467 
468  for (auto i: cpls()) {
469  NameFormat::Map values;
470  values['t'] = "cpl";
471  i->write_xml(_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer, include_mca_subdescriptors);
472  }
473 
474  if (_pkls.empty()) {
475  _pkls.push_back(
476  make_shared<PKL>(
477  standard,
478  _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
479  _new_issue_date.get_value_or(LocalTime().as_string()),
480  _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
481  _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
482  )
483  );
484  }
485 
486  auto pkl = _pkls.front();
487 
488  /* The assets may have changed since we read the PKL, so re-add them */
489  pkl->clear_assets();
490  for (auto asset: assets()) {
491  asset->add_to_pkl(pkl, _directory);
492  }
493 
494  NameFormat::Map values;
495  values['t'] = "pkl";
496  auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
497  pkl->write_xml (pkl_path, signer);
498 
499  if (!_asset_map) {
500  _asset_map = AssetMap(
501  standard,
502  _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
503  _new_issue_date.get_value_or(LocalTime().as_string()),
504  _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
505  _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
506  );
507  }
508 
509  /* The assets may have changed since we read the asset map, so re-add them */
510  _asset_map->clear_assets();
511  _asset_map->add_asset(pkl->id(), pkl_path, true);
512  for (auto asset: assets()) {
513  asset->add_to_assetmap(*_asset_map, _directory);
514  }
515 
516  _asset_map->write_xml(
517  _directory / (standard == Standard::INTEROP ? "ASSETMAP" : "ASSETMAP.xml")
518  );
519 
520  write_volindex (standard);
521 }
522 
523 
524 vector<shared_ptr<CPL>>
525 DCP::cpls () const
526 {
527  return _cpls;
528 }
529 
530 
531 vector<shared_ptr<Asset>>
532 DCP::assets (bool ignore_unresolved) const
533 {
534  vector<shared_ptr<Asset>> assets;
535  for (auto i: cpls()) {
536  assets.push_back (i);
537  for (auto j: i->reel_file_assets()) {
538  if (ignore_unresolved && !j->asset_ref().resolved()) {
539  continue;
540  }
541 
542  auto const id = j->asset_ref().id();
543  if (std::find_if(assets.begin(), assets.end(), [id](shared_ptr<Asset> asset) { return asset->id() == id; }) == assets.end()) {
544  auto o = j->asset_ref().asset();
545  assets.push_back (o);
546  /* More Interop special-casing */
547  auto sub = dynamic_pointer_cast<InteropTextAsset>(o);
548  if (sub) {
549  add_to_container(assets, sub->font_assets());
550  }
551  }
552  }
553  }
554 
555  return assets;
556 }
557 
558 
560 vector<boost::filesystem::path>
561 DCP::directories_from_files (vector<boost::filesystem::path> files)
562 {
563  vector<boost::filesystem::path> d;
564  for (auto i: files) {
565  if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
566  d.push_back (i.parent_path ());
567  }
568  }
569  return d;
570 }
571 
572 
573 void
574 DCP::set_issuer(string issuer)
575 {
576  for (auto pkl: _pkls) {
577  pkl->set_issuer(issuer);
578  }
579  if (_asset_map) {
580  _asset_map->set_issuer(issuer);
581  }
582  _new_issuer = issuer;
583 }
584 
585 
586 void
587 DCP::set_creator(string creator)
588 {
589  for (auto pkl: _pkls) {
590  pkl->set_creator(creator);
591  }
592  if (_asset_map) {
593  _asset_map->set_creator(creator);
594  }
595  _new_creator = creator;
596 }
597 
598 
599 void
600 DCP::set_issue_date(string issue_date)
601 {
602  for (auto pkl: _pkls) {
603  pkl->set_issue_date(issue_date);
604  }
605  if (_asset_map) {
606  _asset_map->set_issue_date(issue_date);
607  }
608  _new_issue_date = issue_date;
609 }
610 
611 
612 void
613 DCP::set_annotation_text(string annotation_text)
614 {
615  for (auto pkl: _pkls) {
616  pkl->set_annotation_text(annotation_text);
617  }
618  if (_asset_map) {
619  _asset_map->set_annotation_text(annotation_text);
620  }
621  _new_annotation_text = annotation_text;
622 }
623 
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:99
static std::vector< boost::filesystem::path > directories_from_files(std::vector< boost::filesystem::path > files)
Definition: dcp.cc:561
bool equals(DCP const &other, EqualityOptions const &options, NoteHandler note) const
Definition: dcp.cc:342
std::vector< std::shared_ptr< Asset > > assets(bool ignore_unresolved=false) const
Definition: dcp.cc:532
std::vector< std::shared_ptr< PKL > > _pkls
Definition: dcp.h:208
boost::filesystem::path _directory
Definition: dcp.h:204
std::vector< std::shared_ptr< CPL > > _cpls
Definition: dcp.h:206
void write_xml(std::shared_ptr< const CertificateChain > signer=std::shared_ptr< const CertificateChain >(), bool include_mca_subdescriptors=true, NameFormat name_format=NameFormat("%t"))
Definition: dcp.cc:452
boost::optional< Standard > standard() const
Definition: dcp.h:166
void read(std::vector< VerificationNote > *notes=nullptr, bool ignore_incorrect_picture_mxf_type=false)
Definition: dcp.cc:140
void write_volindex(Standard standard) const
Definition: dcp.cc:418
An un- or de-crypted key from a KDM.
A decrypted KDM.
Definition: decrypted_kdm.h:75
std::vector< DecryptedKDMKey > keys() const
A class to describe what "equality" means for a particular test.
A representation of a local time (down to the second), including its offset from GMT (equivalent to x...
Definition: local_time.h:68
A miscellaneous exception.
Definition: exceptions.h:94
Thrown when no ASSETMAP was found when trying to read a DCP.
Definition: exceptions.h:181
Any error that occurs when reading data from a DCP.
Definition: exceptions.h:106
An XML error.
Definition: exceptions.h:191
CPL class.
DCP class.
DCP_ASSERT macro.
DecryptedKDM class.
DecryptedKDMKey class.
Exceptions thrown by libdcp.
FontAsset class.
InteropTextAsset class.
J2KPictureAsset class.
MXFMetadata class.
MonoJ2KPictureAsset class.
MonoMPEG2PictureAsset 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)
PKL class.
Methods for conversion to/from string.
ReelAsset class.
ReelTextAsset class.
SMPTETextAsset class.
SoundAsset class.
StereoJ2KPictureAsset class.
Utility methods and classes.
dcp::verify() method and associated code