Add some detail to a verification warning.
[libdcp.git] / src / dcp.cc
1 /*
2     Copyright (C) 2012-2015 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) {
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 (Standard standard, string pkl_uuid, boost::filesystem::path pkl_path, XMLMetadata metadata) const
382 {
383         boost::filesystem::path p = _directory;
384
385         switch (standard) {
386         case INTEROP:
387                 p /= "ASSETMAP";
388                 break;
389         case SMPTE:
390                 p /= "ASSETMAP.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 INTEROP:
401                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
402                 break;
403         case SMPTE:
404                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
405                 break;
406         default:
407                 DCP_ASSERT (false);
408         }
409
410         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
411         root->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
412
413         switch (standard) {
414         case INTEROP:
415                 root->add_child("VolumeCount")->add_child_text ("1");
416                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
417                 root->add_child("Issuer")->add_child_text (metadata.issuer);
418                 root->add_child("Creator")->add_child_text (metadata.creator);
419                 break;
420         case SMPTE:
421                 root->add_child("Creator")->add_child_text (metadata.creator);
422                 root->add_child("VolumeCount")->add_child_text ("1");
423                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
424                 root->add_child("Issuer")->add_child_text (metadata.issuer);
425                 break;
426         default:
427                 DCP_ASSERT (false);
428         }
429
430         xmlpp::Node* asset_list = root->add_child ("AssetList");
431
432         xmlpp::Node* asset = asset_list->add_child ("Asset");
433         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
434         asset->add_child("PackingList")->add_child_text ("true");
435         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
436         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
437         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
438         chunk->add_child("VolumeIndex")->add_child_text ("1");
439         chunk->add_child("Offset")->add_child_text ("0");
440         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
441
442         BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
443                 i->write_to_assetmap (asset_list, _directory);
444         }
445
446         doc.write_to_file_formatted (p.string (), "UTF-8");
447         _asset_map = p;
448 }
449
450 /** Write all the XML files for this DCP.
451  *  @param standand INTEROP or SMPTE.
452  *  @param metadata Metadata to use for PKL and asset map files.
453  *  @param signer Signer to use, or 0.
454  */
455 void
456 DCP::write_xml (
457         Standard standard,
458         XMLMetadata metadata,
459         shared_ptr<const CertificateChain> signer,
460         NameFormat name_format
461         )
462 {
463         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
464                 NameFormat::Map values;
465                 values['t'] = "cpl";
466                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
467         }
468
469         shared_ptr<PKL> pkl;
470
471         if (_pkls.empty()) {
472                 pkl.reset (new PKL (standard, metadata.annotation_text, metadata.issue_date, metadata.issuer, metadata.creator));
473                 _pkls.push_back (pkl);
474                 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
475                         i->add_to_pkl (pkl, _directory);
476                 }
477         } else {
478                 pkl = _pkls.front ();
479         }
480
481         NameFormat::Map values;
482         values['t'] = "pkl";
483         boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
484         pkl->write (pkl_path, signer);
485
486         write_volindex (standard);
487         write_assetmap (standard, pkl->id(), pkl_path, metadata);
488 }
489
490 list<shared_ptr<CPL> >
491 DCP::cpls () const
492 {
493         return _cpls;
494 }
495
496 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
497  *  an exception is thrown if they are found.
498  *  @return All assets (including CPLs).
499  */
500 list<shared_ptr<Asset> >
501 DCP::assets (bool ignore_unresolved) const
502 {
503         list<shared_ptr<Asset> > assets;
504         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
505                 assets.push_back (i);
506                 BOOST_FOREACH (shared_ptr<const ReelMXF> j, i->reel_mxfs()) {
507                         if (ignore_unresolved && !j->asset_ref().resolved()) {
508                                 continue;
509                         }
510                         shared_ptr<Asset> o = j->asset_ref().asset ();
511                         assets.push_back (o);
512                         /* More Interop special-casing */
513                         shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
514                         if (sub) {
515                                 sub->add_font_assets (assets);
516                         }
517                 }
518         }
519
520         return assets;
521 }
522
523 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
524 vector<boost::filesystem::path>
525 DCP::directories_from_files (vector<boost::filesystem::path> files)
526 {
527         vector<boost::filesystem::path> d;
528         BOOST_FOREACH (boost::filesystem::path i, files) {
529                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
530                         d.push_back (i.parent_path ());
531                 }
532         }
533         return d;
534 }