Simplify the implementation of DCP::add.
[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                         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         for (auto cpl: cpls()) {
368                 if (std::any_of(keys.begin(), keys.end(), [cpl](DecryptedKDMKey const& key) { return key.cpl_id() == cpl->id(); })) {
369                         cpl->add (kdm);
370                 }
371         }
372 }
373
374
375 /** Write the VOLINDEX file.
376  *  @param standard DCP standard to use (INTEROP or SMPTE)
377  */
378 void
379 DCP::write_volindex (Standard standard) const
380 {
381         auto p = _directory;
382         switch (standard) {
383         case Standard::INTEROP:
384                 p /= "VOLINDEX";
385                 break;
386         case Standard::SMPTE:
387                 p /= "VOLINDEX.xml";
388                 break;
389         default:
390                 DCP_ASSERT (false);
391         }
392
393         xmlpp::Document doc;
394         xmlpp::Element* root;
395
396         switch (standard) {
397         case Standard::INTEROP:
398                 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
399                 break;
400         case Standard::SMPTE:
401                 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
402                 break;
403         default:
404                 DCP_ASSERT (false);
405         }
406
407         root->add_child("Index")->add_child_text ("1");
408         doc.write_to_file_formatted (p.string (), "UTF-8");
409 }
410
411
412 void
413 DCP::write_assetmap (
414         Standard standard, string pkl_uuid, boost::filesystem::path pkl_path,
415         string issuer, string creator, string issue_date, string annotation_text
416         ) const
417 {
418         auto p = _directory;
419
420         switch (standard) {
421         case Standard::INTEROP:
422                 p /= "ASSETMAP";
423                 break;
424         case Standard::SMPTE:
425                 p /= "ASSETMAP.xml";
426                 break;
427         default:
428                 DCP_ASSERT (false);
429         }
430
431         xmlpp::Document doc;
432         xmlpp::Element* root;
433
434         switch (standard) {
435         case Standard::INTEROP:
436                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
437                 break;
438         case Standard::SMPTE:
439                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
440                 break;
441         default:
442                 DCP_ASSERT (false);
443         }
444
445         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
446         root->add_child("AnnotationText")->add_child_text (annotation_text);
447
448         switch (standard) {
449         case Standard::INTEROP:
450                 root->add_child("VolumeCount")->add_child_text ("1");
451                 root->add_child("IssueDate")->add_child_text (issue_date);
452                 root->add_child("Issuer")->add_child_text (issuer);
453                 root->add_child("Creator")->add_child_text (creator);
454                 break;
455         case Standard::SMPTE:
456                 root->add_child("Creator")->add_child_text (creator);
457                 root->add_child("VolumeCount")->add_child_text ("1");
458                 root->add_child("IssueDate")->add_child_text (issue_date);
459                 root->add_child("Issuer")->add_child_text (issuer);
460                 break;
461         default:
462                 DCP_ASSERT (false);
463         }
464
465         auto asset_list = root->add_child ("AssetList");
466
467         auto asset = asset_list->add_child ("Asset");
468         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
469         asset->add_child("PackingList")->add_child_text ("true");
470         auto chunk_list = asset->add_child ("ChunkList");
471         auto chunk = chunk_list->add_child ("Chunk");
472         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
473         chunk->add_child("VolumeIndex")->add_child_text ("1");
474         chunk->add_child("Offset")->add_child_text ("0");
475         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
476
477         for (auto i: assets()) {
478                 i->write_to_assetmap (asset_list, _directory);
479         }
480
481         doc.write_to_file_formatted (p.string (), "UTF-8");
482         _asset_map = p;
483 }
484
485
486 void
487 DCP::write_xml (
488         string issuer,
489         string creator,
490         string issue_date,
491         string annotation_text,
492         shared_ptr<const CertificateChain> signer,
493         NameFormat name_format
494         )
495 {
496         if (_cpls.empty()) {
497                 throw MiscError ("Cannot write DCP with no CPLs.");
498         }
499
500         auto standard = std::accumulate (
501                 std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
502                 [](Standard s, shared_ptr<CPL> c) {
503                         if (s != c->standard()) {
504                                 throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
505                         }
506                         return s;
507                 }
508                 );
509
510         for (auto i: cpls()) {
511                 NameFormat::Map values;
512                 values['t'] = "cpl";
513                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer);
514         }
515
516         shared_ptr<PKL> pkl;
517
518         if (_pkls.empty()) {
519                 pkl = make_shared<PKL>(standard, annotation_text, issue_date, issuer, creator);
520                 _pkls.push_back (pkl);
521                 for (auto i: assets()) {
522                         i->add_to_pkl (pkl, _directory);
523                 }
524         } else {
525                 pkl = _pkls.front ();
526         }
527
528         NameFormat::Map values;
529         values['t'] = "pkl";
530         auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
531         pkl->write (pkl_path, signer);
532
533         write_volindex (standard);
534         write_assetmap (standard, pkl->id(), pkl_path, issuer, creator, issue_date, annotation_text);
535 }
536
537
538 vector<shared_ptr<CPL>>
539 DCP::cpls () const
540 {
541         return _cpls;
542 }
543
544
545 vector<shared_ptr<Asset>>
546 DCP::assets (bool ignore_unresolved) const
547 {
548         vector<shared_ptr<Asset>> assets;
549         for (auto i: cpls()) {
550                 assets.push_back (i);
551                 for (auto j: i->reel_file_assets()) {
552                         if (ignore_unresolved && !j->asset_ref().resolved()) {
553                                 continue;
554                         }
555
556                         auto const id = j->asset_ref().id();
557                         auto already_got = false;
558                         for (auto k: assets) {
559                                 if (k->id() == id) {
560                                         already_got = true;
561                                 }
562                         }
563
564                         if (!already_got) {
565                                 auto o = j->asset_ref().asset();
566                                 assets.push_back (o);
567                                 /* More Interop special-casing */
568                                 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
569                                 if (sub) {
570                                         sub->add_font_assets (assets);
571                                 }
572                         }
573                 }
574         }
575
576         return assets;
577 }
578
579
580 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
581 vector<boost::filesystem::path>
582 DCP::directories_from_files (vector<boost::filesystem::path> files)
583 {
584         vector<boost::filesystem::path> d;
585         for (auto i: files) {
586                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
587                         d.push_back (i.parent_path ());
588                 }
589         }
590         return d;
591 }