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