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