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