c72bef63680f56b71f80ac4aa5cf50b79c1e0d6b
[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                 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
225                         auto p = new xmlpp::DomParser;
226                         try {
227                                 p->parse_file (path.string());
228                         } catch (std::exception& e) {
229                                 delete p;
230                                 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
231                         }
232
233                         auto const root = p->get_document()->get_root_node()->get_name();
234                         delete p;
235
236                         if (root == "CompositionPlaylist") {
237                                 auto cpl = make_shared<CPL>(path);
238                                 if (_standard && cpl->standard() != _standard.get() && notes) {
239                                         notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
240                                 }
241                                 _cpls.push_back (cpl);
242                         } else if (root == "DCSubtitle") {
243                                 if (_standard && _standard.get() == Standard::SMPTE && notes) {
244                                         notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
245                                 }
246                                 other_assets.push_back (make_shared<InteropSubtitleAsset>(path));
247                         }
248                 } else if (
249                         *pkl_type == PictureAsset::static_pkl_type(*_standard) ||
250                         *pkl_type == SoundAsset::static_pkl_type(*_standard) ||
251                         *pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
252                         *pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
253                         ) {
254
255                         other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type));
256                 } else if (*pkl_type == FontAsset::static_pkl_type(*_standard)) {
257                         other_assets.push_back (make_shared<FontAsset>(i.first, path));
258                 } else if (*pkl_type == "image/png") {
259                         /* It's an Interop PNG subtitle; let it go */
260                 } else {
261                         throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
262                 }
263         }
264
265         resolve_refs (other_assets);
266
267         /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
268         if (notes) {
269                 for (auto i: cpls()) {
270                         for (auto j: i->reel_file_assets()) {
271                                 if (!j->asset_ref().resolved() && paths.find(j->asset_ref().id()) == paths.end()) {
272                                         notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
273                                 }
274                         }
275                 }
276         }
277 }
278
279
280 void
281 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
282 {
283         for (auto i: cpls()) {
284                 i->resolve_refs (assets);
285         }
286 }
287
288
289 bool
290 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
291 {
292         auto a = cpls ();
293         auto b = other.cpls ();
294
295         if (a.size() != b.size()) {
296                 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
297                 return false;
298         }
299
300         bool r = true;
301
302         for (auto i: a) {
303                 auto j = b.begin();
304                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
305                         ++j;
306                 }
307
308                 if (j == b.end ()) {
309                         r = false;
310                 }
311         }
312
313         return r;
314 }
315
316
317 void
318 DCP::add (shared_ptr<CPL> cpl)
319 {
320         _cpls.push_back (cpl);
321 }
322
323
324 bool
325 DCP::any_encrypted () const
326 {
327         for (auto i: cpls()) {
328                 if (i->any_encrypted()) {
329                         return true;
330                 }
331         }
332
333         return false;
334 }
335
336
337 bool
338 DCP::all_encrypted () const
339 {
340         for (auto i: cpls()) {
341                 if (!i->all_encrypted()) {
342                         return false;
343                 }
344         }
345
346         return true;
347 }
348
349
350 void
351 DCP::add (DecryptedKDM const & kdm)
352 {
353         auto keys = kdm.keys ();
354
355         for (auto i: cpls()) {
356                 for (auto const& j: kdm.keys()) {
357                         if (j.cpl_id() == i->id()) {
358                                 i->add (kdm);
359                         }
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_assetmap (
404         Standard standard, string pkl_uuid, boost::filesystem::path pkl_path,
405         string issuer, string creator, string issue_date, string annotation_text
406         ) const
407 {
408         auto p = _directory;
409
410         switch (standard) {
411         case Standard::INTEROP:
412                 p /= "ASSETMAP";
413                 break;
414         case Standard::SMPTE:
415                 p /= "ASSETMAP.xml";
416                 break;
417         default:
418                 DCP_ASSERT (false);
419         }
420
421         xmlpp::Document doc;
422         xmlpp::Element* root;
423
424         switch (standard) {
425         case Standard::INTEROP:
426                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
427                 break;
428         case Standard::SMPTE:
429                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
430                 break;
431         default:
432                 DCP_ASSERT (false);
433         }
434
435         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
436         root->add_child("AnnotationText")->add_child_text (annotation_text);
437
438         switch (standard) {
439         case Standard::INTEROP:
440                 root->add_child("VolumeCount")->add_child_text ("1");
441                 root->add_child("IssueDate")->add_child_text (issue_date);
442                 root->add_child("Issuer")->add_child_text (issuer);
443                 root->add_child("Creator")->add_child_text (creator);
444                 break;
445         case Standard::SMPTE:
446                 root->add_child("Creator")->add_child_text (creator);
447                 root->add_child("VolumeCount")->add_child_text ("1");
448                 root->add_child("IssueDate")->add_child_text (issue_date);
449                 root->add_child("Issuer")->add_child_text (issuer);
450                 break;
451         default:
452                 DCP_ASSERT (false);
453         }
454
455         auto asset_list = root->add_child ("AssetList");
456
457         auto asset = asset_list->add_child ("Asset");
458         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
459         asset->add_child("PackingList")->add_child_text ("true");
460         auto chunk_list = asset->add_child ("ChunkList");
461         auto chunk = chunk_list->add_child ("Chunk");
462         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
463         chunk->add_child("VolumeIndex")->add_child_text ("1");
464         chunk->add_child("Offset")->add_child_text ("0");
465         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
466
467         for (auto i: assets()) {
468                 i->write_to_assetmap (asset_list, _directory);
469         }
470
471         doc.write_to_file_formatted (p.string (), "UTF-8");
472         _asset_map = p;
473 }
474
475
476 void
477 DCP::write_xml (
478         string issuer,
479         string creator,
480         string issue_date,
481         string annotation_text,
482         shared_ptr<const CertificateChain> signer,
483         NameFormat name_format
484         )
485 {
486         if (_cpls.empty()) {
487                 throw MiscError ("Cannot write DCP with no CPLs.");
488         }
489
490         auto standard = std::accumulate (
491                 std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
492                 [](Standard s, shared_ptr<CPL> c) {
493                         if (s != c->standard()) {
494                                 throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
495                         }
496                         return s;
497                 }
498                 );
499
500         for (auto i: cpls()) {
501                 NameFormat::Map values;
502                 values['t'] = "cpl";
503                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer);
504         }
505
506         shared_ptr<PKL> pkl;
507
508         if (_pkls.empty()) {
509                 pkl = make_shared<PKL>(standard, annotation_text, issue_date, issuer, creator);
510                 _pkls.push_back (pkl);
511                 for (auto i: assets()) {
512                         i->add_to_pkl (pkl, _directory);
513                 }
514         } else {
515                 pkl = _pkls.front ();
516         }
517
518         NameFormat::Map values;
519         values['t'] = "pkl";
520         auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
521         pkl->write (pkl_path, signer);
522
523         write_volindex (standard);
524         write_assetmap (standard, pkl->id(), pkl_path, issuer, creator, issue_date, annotation_text);
525 }
526
527
528 vector<shared_ptr<CPL>>
529 DCP::cpls () const
530 {
531         return _cpls;
532 }
533
534
535 vector<shared_ptr<Asset>>
536 DCP::assets (bool ignore_unresolved) const
537 {
538         vector<shared_ptr<Asset>> assets;
539         for (auto i: cpls()) {
540                 assets.push_back (i);
541                 for (auto j: i->reel_file_assets()) {
542                         if (ignore_unresolved && !j->asset_ref().resolved()) {
543                                 continue;
544                         }
545
546                         auto const id = j->asset_ref().id();
547                         auto already_got = false;
548                         for (auto k: assets) {
549                                 if (k->id() == id) {
550                                         already_got = true;
551                                 }
552                         }
553
554                         if (!already_got) {
555                                 auto o = j->asset_ref().asset();
556                                 assets.push_back (o);
557                                 /* More Interop special-casing */
558                                 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
559                                 if (sub) {
560                                         sub->add_font_assets (assets);
561                                 }
562                         }
563                 }
564         }
565
566         return assets;
567 }
568
569
570 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
571 vector<boost::filesystem::path>
572 DCP::directories_from_files (vector<boost::filesystem::path> files)
573 {
574         vector<boost::filesystem::path> d;
575         for (auto i: files) {
576                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
577                         d.push_back (i.parent_path ());
578                 }
579         }
580         return d;
581 }