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