Ignore parameters in PKL type strings when checking them.
[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/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                         other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type));
265                 } else if (*pkl_type == remove_parameters(FontAsset::static_pkl_type(*_standard))) {
266                         other_assets.push_back (make_shared<FontAsset>(i.first, path));
267                 } else if (*pkl_type == "image/png") {
268                         /* It's an Interop PNG subtitle; let it go */
269                 } else {
270                         throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
271                 }
272         }
273
274         resolve_refs (other_assets);
275
276         /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
277         if (notes) {
278                 for (auto i: cpls()) {
279                         for (auto j: i->reel_file_assets()) {
280                                 if (!j->asset_ref().resolved() && paths.find(j->asset_ref().id()) == paths.end()) {
281                                         notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
282                                 }
283                         }
284                 }
285         }
286 }
287
288
289 void
290 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
291 {
292         for (auto i: cpls()) {
293                 i->resolve_refs (assets);
294         }
295 }
296
297
298 bool
299 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
300 {
301         auto a = cpls ();
302         auto b = other.cpls ();
303
304         if (a.size() != b.size()) {
305                 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
306                 return false;
307         }
308
309         bool r = true;
310
311         for (auto i: a) {
312                 auto j = b.begin();
313                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
314                         ++j;
315                 }
316
317                 if (j == b.end ()) {
318                         r = false;
319                 }
320         }
321
322         return r;
323 }
324
325
326 void
327 DCP::add (shared_ptr<CPL> cpl)
328 {
329         _cpls.push_back (cpl);
330 }
331
332
333 bool
334 DCP::any_encrypted () const
335 {
336         for (auto i: cpls()) {
337                 if (i->any_encrypted()) {
338                         return true;
339                 }
340         }
341
342         return false;
343 }
344
345
346 bool
347 DCP::all_encrypted () const
348 {
349         for (auto i: cpls()) {
350                 if (!i->all_encrypted()) {
351                         return false;
352                 }
353         }
354
355         return true;
356 }
357
358
359 void
360 DCP::add (DecryptedKDM const & kdm)
361 {
362         auto keys = kdm.keys ();
363
364         for (auto i: cpls()) {
365                 for (auto const& j: kdm.keys()) {
366                         if (j.cpl_id() == i->id()) {
367                                 i->add (kdm);
368                         }
369                 }
370         }
371 }
372
373
374 /** Write the VOLINDEX file.
375  *  @param standard DCP standard to use (INTEROP or SMPTE)
376  */
377 void
378 DCP::write_volindex (Standard standard) const
379 {
380         auto p = _directory;
381         switch (standard) {
382         case Standard::INTEROP:
383                 p /= "VOLINDEX";
384                 break;
385         case Standard::SMPTE:
386                 p /= "VOLINDEX.xml";
387                 break;
388         default:
389                 DCP_ASSERT (false);
390         }
391
392         xmlpp::Document doc;
393         xmlpp::Element* root;
394
395         switch (standard) {
396         case Standard::INTEROP:
397                 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
398                 break;
399         case Standard::SMPTE:
400                 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
401                 break;
402         default:
403                 DCP_ASSERT (false);
404         }
405
406         root->add_child("Index")->add_child_text ("1");
407         doc.write_to_file_formatted (p.string (), "UTF-8");
408 }
409
410
411 void
412 DCP::write_assetmap (
413         Standard standard, string pkl_uuid, boost::filesystem::path pkl_path,
414         string issuer, string creator, string issue_date, string annotation_text
415         ) const
416 {
417         auto p = _directory;
418
419         switch (standard) {
420         case Standard::INTEROP:
421                 p /= "ASSETMAP";
422                 break;
423         case Standard::SMPTE:
424                 p /= "ASSETMAP.xml";
425                 break;
426         default:
427                 DCP_ASSERT (false);
428         }
429
430         xmlpp::Document doc;
431         xmlpp::Element* root;
432
433         switch (standard) {
434         case Standard::INTEROP:
435                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
436                 break;
437         case Standard::SMPTE:
438                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
439                 break;
440         default:
441                 DCP_ASSERT (false);
442         }
443
444         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
445         root->add_child("AnnotationText")->add_child_text (annotation_text);
446
447         switch (standard) {
448         case Standard::INTEROP:
449                 root->add_child("VolumeCount")->add_child_text ("1");
450                 root->add_child("IssueDate")->add_child_text (issue_date);
451                 root->add_child("Issuer")->add_child_text (issuer);
452                 root->add_child("Creator")->add_child_text (creator);
453                 break;
454         case Standard::SMPTE:
455                 root->add_child("Creator")->add_child_text (creator);
456                 root->add_child("VolumeCount")->add_child_text ("1");
457                 root->add_child("IssueDate")->add_child_text (issue_date);
458                 root->add_child("Issuer")->add_child_text (issuer);
459                 break;
460         default:
461                 DCP_ASSERT (false);
462         }
463
464         auto asset_list = root->add_child ("AssetList");
465
466         auto asset = asset_list->add_child ("Asset");
467         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
468         asset->add_child("PackingList")->add_child_text ("true");
469         auto chunk_list = asset->add_child ("ChunkList");
470         auto chunk = chunk_list->add_child ("Chunk");
471         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
472         chunk->add_child("VolumeIndex")->add_child_text ("1");
473         chunk->add_child("Offset")->add_child_text ("0");
474         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
475
476         for (auto i: assets()) {
477                 i->write_to_assetmap (asset_list, _directory);
478         }
479
480         doc.write_to_file_formatted (p.string (), "UTF-8");
481         _asset_map = p;
482 }
483
484
485 void
486 DCP::write_xml (
487         string issuer,
488         string creator,
489         string issue_date,
490         string annotation_text,
491         shared_ptr<const CertificateChain> signer,
492         NameFormat name_format
493         )
494 {
495         if (_cpls.empty()) {
496                 throw MiscError ("Cannot write DCP with no CPLs.");
497         }
498
499         auto standard = std::accumulate (
500                 std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
501                 [](Standard s, shared_ptr<CPL> c) {
502                         if (s != c->standard()) {
503                                 throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
504                         }
505                         return s;
506                 }
507                 );
508
509         for (auto i: cpls()) {
510                 NameFormat::Map values;
511                 values['t'] = "cpl";
512                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer);
513         }
514
515         shared_ptr<PKL> pkl;
516
517         if (_pkls.empty()) {
518                 pkl = make_shared<PKL>(standard, annotation_text, issue_date, issuer, creator);
519                 _pkls.push_back (pkl);
520                 for (auto i: assets()) {
521                         i->add_to_pkl (pkl, _directory);
522                 }
523         } else {
524                 pkl = _pkls.front ();
525         }
526
527         NameFormat::Map values;
528         values['t'] = "pkl";
529         auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
530         pkl->write (pkl_path, signer);
531
532         write_volindex (standard);
533         write_assetmap (standard, pkl->id(), pkl_path, issuer, creator, issue_date, annotation_text);
534 }
535
536
537 vector<shared_ptr<CPL>>
538 DCP::cpls () const
539 {
540         return _cpls;
541 }
542
543
544 vector<shared_ptr<Asset>>
545 DCP::assets (bool ignore_unresolved) const
546 {
547         vector<shared_ptr<Asset>> assets;
548         for (auto i: cpls()) {
549                 assets.push_back (i);
550                 for (auto j: i->reel_file_assets()) {
551                         if (ignore_unresolved && !j->asset_ref().resolved()) {
552                                 continue;
553                         }
554
555                         auto const id = j->asset_ref().id();
556                         auto already_got = false;
557                         for (auto k: assets) {
558                                 if (k->id() == id) {
559                                         already_got = true;
560                                 }
561                         }
562
563                         if (!already_got) {
564                                 auto o = j->asset_ref().asset();
565                                 assets.push_back (o);
566                                 /* More Interop special-casing */
567                                 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
568                                 if (sub) {
569                                         sub->add_font_assets (assets);
570                                 }
571                         }
572                 }
573         }
574
575         return assets;
576 }
577
578
579 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
580 vector<boost::filesystem::path>
581 DCP::directories_from_files (vector<boost::filesystem::path> files)
582 {
583         vector<boost::filesystem::path> d;
584         for (auto i: files) {
585                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
586                         d.push_back (i.parent_path ());
587                 }
588         }
589         return d;
590 }