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