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