Fix thinko.
[libdcp.git] / src / dcp.cc
1 /*
2     Copyright (C) 2012-2020 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 #include <boost/foreach.hpp>
69
70 using std::string;
71 using std::list;
72 using std::vector;
73 using std::cout;
74 using std::make_pair;
75 using std::map;
76 using std::cerr;
77 using std::exception;
78 using boost::shared_ptr;
79 using boost::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 list.
109  */
110 void
111 DCP::read (list<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 = INTEROP;
128         } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
129                 _standard = SMPTE;
130         } else {
131                 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
132         }
133
134         list<shared_ptr<cxml::Node> > asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
135         map<string, boost::filesystem::path> paths;
136         list<boost::filesystem::path> pkl_paths;
137         BOOST_FOREACH (shared_ptr<cxml::Node> 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                 string 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 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 SMPTE:
154                 {
155                         optional<string> 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         BOOST_FOREACH (boost::filesystem::path i, pkl_paths) {
171                 _pkls.push_back (shared_ptr<PKL>(new 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         list<shared_ptr<Asset> > other_assets;
185
186         for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
187                 boost::filesystem::path 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(VerificationNote::VERIFY_WARNING, VerificationNote::EMPTY_ASSET_PATH));
196                         }
197                         continue;
198                 }
199
200                 if (!boost::filesystem::exists(path)) {
201                         if (notes) {
202                                 notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::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                 BOOST_FOREACH (shared_ptr<PKL> j, _pkls) {
210                         pkl_type = j->type(i->first);
211                         if (pkl_type) {
212                                 break;
213                         }
214                 }
215
216                 DCP_ASSERT (pkl_type);
217
218                 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
219                         xmlpp::DomParser* p = new xmlpp::DomParser;
220                         try {
221                                 p->parse_file (path.string());
222                         } catch (std::exception& e) {
223                                 delete p;
224                                 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
225                         }
226
227                         string const root = p->get_document()->get_root_node()->get_name ();
228                         delete p;
229
230                         if (root == "CompositionPlaylist") {
231                                 shared_ptr<CPL> cpl (new CPL (path));
232                                 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get() && notes) {
233                                         notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
234                                 }
235                                 _cpls.push_back (cpl);
236                         } else if (root == "DCSubtitle") {
237                                 if (_standard && _standard.get() == SMPTE && notes) {
238                                         notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
239                                 }
240                                 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
241                         }
242                 } else if (
243                         *pkl_type == PictureAsset::static_pkl_type(*_standard) ||
244                         *pkl_type == SoundAsset::static_pkl_type(*_standard) ||
245                         *pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
246                         *pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
247                         ) {
248
249                         other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type));
250                 } else if (*pkl_type == FontAsset::static_pkl_type(*_standard)) {
251                         other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
252                 } else if (*pkl_type == "image/png") {
253                         /* It's an Interop PNG subtitle; let it go */
254                 } else {
255                         throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
256                 }
257         }
258
259         resolve_refs (other_assets);
260
261         /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
262         if (notes) {
263                 BOOST_FOREACH (shared_ptr<CPL> i, cpls()) {
264                         BOOST_FOREACH (shared_ptr<const ReelMXF> j, i->reel_mxfs()) {
265                                 if (!j->asset_ref().resolved() && paths.find(j->asset_ref().id()) == paths.end()) {
266                                         notes->push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::EXTERNAL_ASSET, j->asset_ref().id()));
267                                 }
268                         }
269                 }
270         }
271 }
272
273 void
274 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
275 {
276         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
277                 i->resolve_refs (assets);
278         }
279 }
280
281 bool
282 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
283 {
284         list<shared_ptr<CPL> > a = cpls ();
285         list<shared_ptr<CPL> > b = other.cpls ();
286
287         if (a.size() != b.size()) {
288                 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
289                 return false;
290         }
291
292         bool r = true;
293
294         BOOST_FOREACH (shared_ptr<CPL> i, a) {
295                 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
296                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
297                         ++j;
298                 }
299
300                 if (j == b.end ()) {
301                         r = false;
302                 }
303         }
304
305         return r;
306 }
307
308 void
309 DCP::add (boost::shared_ptr<CPL> cpl)
310 {
311         _cpls.push_back (cpl);
312 }
313
314 bool
315 DCP::encrypted () const
316 {
317         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
318                 if (i->encrypted ()) {
319                         return true;
320                 }
321         }
322
323         return false;
324 }
325
326 /** Add a KDM to decrypt this DCP.  This method must be called after DCP::read()
327  *  or the KDM you specify will be ignored.
328  *  @param kdm KDM to use.
329  */
330 void
331 DCP::add (DecryptedKDM const & kdm)
332 {
333         list<DecryptedKDMKey> keys = kdm.keys ();
334
335         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
336                 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
337                         if (j.cpl_id() == i->id()) {
338                                 i->add (kdm);
339                         }
340                 }
341         }
342 }
343
344 /** Write the VOLINDEX file.
345  *  @param standard DCP standard to use (INTEROP or SMPTE)
346  */
347 void
348 DCP::write_volindex (Standard standard) const
349 {
350         boost::filesystem::path p = _directory;
351         switch (standard) {
352         case INTEROP:
353                 p /= "VOLINDEX";
354                 break;
355         case SMPTE:
356                 p /= "VOLINDEX.xml";
357                 break;
358         default:
359                 DCP_ASSERT (false);
360         }
361
362         xmlpp::Document doc;
363         xmlpp::Element* root;
364
365         switch (standard) {
366         case INTEROP:
367                 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
368                 break;
369         case SMPTE:
370                 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
371                 break;
372         default:
373                 DCP_ASSERT (false);
374         }
375
376         root->add_child("Index")->add_child_text ("1");
377         doc.write_to_file_formatted (p.string (), "UTF-8");
378 }
379
380 void
381 DCP::write_assetmap (
382         Standard standard, string pkl_uuid, boost::filesystem::path pkl_path,
383         string issuer, string creator, string issue_date, string annotation_text
384         ) const
385 {
386         boost::filesystem::path p = _directory;
387
388         switch (standard) {
389         case INTEROP:
390                 p /= "ASSETMAP";
391                 break;
392         case SMPTE:
393                 p /= "ASSETMAP.xml";
394                 break;
395         default:
396                 DCP_ASSERT (false);
397         }
398
399         xmlpp::Document doc;
400         xmlpp::Element* root;
401
402         switch (standard) {
403         case INTEROP:
404                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
405                 break;
406         case SMPTE:
407                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
408                 break;
409         default:
410                 DCP_ASSERT (false);
411         }
412
413         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
414         root->add_child("AnnotationText")->add_child_text (annotation_text);
415
416         switch (standard) {
417         case INTEROP:
418                 root->add_child("VolumeCount")->add_child_text ("1");
419                 root->add_child("IssueDate")->add_child_text (issue_date);
420                 root->add_child("Issuer")->add_child_text (issuer);
421                 root->add_child("Creator")->add_child_text (creator);
422                 break;
423         case SMPTE:
424                 root->add_child("Creator")->add_child_text (creator);
425                 root->add_child("VolumeCount")->add_child_text ("1");
426                 root->add_child("IssueDate")->add_child_text (issue_date);
427                 root->add_child("Issuer")->add_child_text (issuer);
428                 break;
429         default:
430                 DCP_ASSERT (false);
431         }
432
433         xmlpp::Node* asset_list = root->add_child ("AssetList");
434
435         xmlpp::Node* asset = asset_list->add_child ("Asset");
436         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
437         asset->add_child("PackingList")->add_child_text ("true");
438         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
439         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
440         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
441         chunk->add_child("VolumeIndex")->add_child_text ("1");
442         chunk->add_child("Offset")->add_child_text ("0");
443         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
444
445         BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
446                 i->write_to_assetmap (asset_list, _directory);
447         }
448
449         doc.write_to_file_formatted (p.string (), "UTF-8");
450         _asset_map = p;
451 }
452
453 /** Write all the XML files for this DCP.
454  *  @param standand INTEROP or SMPTE.
455  *  @param metadata Metadata to use for PKL and asset map files.
456  *  @param signer Signer to use, or 0.
457  */
458 void
459 DCP::write_xml (
460         Standard standard,
461         string issuer,
462         string creator,
463         string issue_date,
464         string annotation_text,
465         shared_ptr<const CertificateChain> signer,
466         NameFormat name_format
467         )
468 {
469         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
470                 NameFormat::Map values;
471                 values['t'] = "cpl";
472                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
473         }
474
475         shared_ptr<PKL> pkl;
476
477         if (_pkls.empty()) {
478                 pkl.reset (new PKL(standard, annotation_text, issue_date, issuer, creator));
479                 _pkls.push_back (pkl);
480                 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
481                         i->add_to_pkl (pkl, _directory);
482                 }
483         } else {
484                 pkl = _pkls.front ();
485         }
486
487         NameFormat::Map values;
488         values['t'] = "pkl";
489         boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
490         pkl->write (pkl_path, signer);
491
492         write_volindex (standard);
493         write_assetmap (standard, pkl->id(), pkl_path, issuer, creator, issue_date, annotation_text);
494 }
495
496 list<shared_ptr<CPL> >
497 DCP::cpls () const
498 {
499         return _cpls;
500 }
501
502 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
503  *  an exception is thrown if they are found.
504  *  @return All assets (including CPLs).
505  */
506 list<shared_ptr<Asset> >
507 DCP::assets (bool ignore_unresolved) const
508 {
509         list<shared_ptr<Asset> > assets;
510         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
511                 assets.push_back (i);
512                 BOOST_FOREACH (shared_ptr<const ReelMXF> j, i->reel_mxfs()) {
513                         if (ignore_unresolved && !j->asset_ref().resolved()) {
514                                 continue;
515                         }
516
517                         string const id = j->asset_ref().id();
518                         bool already_got = false;
519                         BOOST_FOREACH (shared_ptr<Asset> k, assets) {
520                                 if (k->id() == id) {
521                                         already_got = true;
522                                 }
523                         }
524
525                         if (!already_got) {
526                                 shared_ptr<Asset> o = j->asset_ref().asset();
527                                 assets.push_back (o);
528                                 /* More Interop special-casing */
529                                 shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
530                                 if (sub) {
531                                         sub->add_font_assets (assets);
532                                 }
533                         }
534                 }
535         }
536
537         return assets;
538 }
539
540 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
541 vector<boost::filesystem::path>
542 DCP::directories_from_files (vector<boost::filesystem::path> files)
543 {
544         vector<boost::filesystem::path> d;
545         BOOST_FOREACH (boost::filesystem::path i, files) {
546                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
547                         d.push_back (i.parent_path ());
548                 }
549         }
550         return d;
551 }