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