Use VerificationNote for non-fatal errors in DCP::read.
[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         boost::filesystem::path asset_map_file;
116         if (boost::filesystem::exists (_directory / "ASSETMAP")) {
117                 asset_map_file = _directory / "ASSETMAP";
118         } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
119                 asset_map_file = _directory / "ASSETMAP.xml";
120         } else {
121                 boost::throw_exception (DCPReadError (String::compose ("could not find ASSETMAP nor ASSETMAP.xml in `%1'", _directory.string())));
122         }
123
124         cxml::Document asset_map ("AssetMap");
125
126         asset_map.read_file (asset_map_file);
127         if (asset_map.namespace_uri() == assetmap_interop_ns) {
128                 _standard = INTEROP;
129         } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
130                 _standard = SMPTE;
131         } else {
132                 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
133         }
134
135         list<shared_ptr<cxml::Node> > asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
136         map<string, boost::filesystem::path> paths;
137         list<boost::filesystem::path> pkl_paths;
138         BOOST_FOREACH (shared_ptr<cxml::Node> 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                 string 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 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 SMPTE:
155                 {
156                         optional<string> 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         BOOST_FOREACH (boost::filesystem::path i, pkl_paths) {
172                 _pkls.push_back (shared_ptr<PKL>(new PKL(_directory / i)));
173         }
174
175         /* Now we have:
176              paths - files in the DCP that are not PKLs.
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         list<shared_ptr<Asset> > other_assets;
186
187         for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
188                 boost::filesystem::path 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(VerificationNote::VERIFY_WARNING, VerificationNote::EMPTY_ASSET_PATH));
197                         }
198                         continue;
199                 }
200
201                 if (!boost::filesystem::exists(path)) {
202                         if (notes) {
203                                 notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISSING_ASSET));
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                 BOOST_FOREACH (shared_ptr<PKL> j, _pkls) {
211                         pkl_type = j->type(i->first);
212                         if (pkl_type) {
213                                 break;
214                         }
215                 }
216
217                 DCP_ASSERT (pkl_type);
218
219                 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
220                         xmlpp::DomParser* p = new xmlpp::DomParser;
221                         try {
222                                 p->parse_file (path.string());
223                         } catch (std::exception& e) {
224                                 delete p;
225                                 throw DCPReadError(String::compose("XML error in %1", path.string()), e.what());
226                         }
227
228                         string const root = p->get_document()->get_root_node()->get_name ();
229                         delete p;
230
231                         if (root == "CompositionPlaylist") {
232                                 shared_ptr<CPL> cpl (new CPL (path));
233                                 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get() && notes) {
234                                         notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
235                                 }
236                                 _cpls.push_back (cpl);
237                         } else if (root == "DCSubtitle") {
238                                 if (_standard && _standard.get() == SMPTE) {
239                                         notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
240                                 }
241                                 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
242                         }
243                 } else if (
244                         *pkl_type == PictureAsset::static_pkl_type(*_standard) ||
245                         *pkl_type == SoundAsset::static_pkl_type(*_standard) ||
246                         *pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
247                         *pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
248                         ) {
249
250                         other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type));
251                 } else if (*pkl_type == FontAsset::static_pkl_type(*_standard)) {
252                         other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
253                 } else if (*pkl_type == "image/png") {
254                         /* It's an Interop PNG subtitle; let it go */
255                 } else {
256                         throw DCPReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
257                 }
258         }
259
260         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
261                 i->resolve_refs (other_assets);
262         }
263 }
264
265 void
266 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
267 {
268         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
269                 i->resolve_refs (assets);
270         }
271 }
272
273 bool
274 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
275 {
276         list<shared_ptr<CPL> > a = cpls ();
277         list<shared_ptr<CPL> > b = other.cpls ();
278
279         if (a.size() != b.size()) {
280                 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
281                 return false;
282         }
283
284         bool r = true;
285
286         BOOST_FOREACH (shared_ptr<CPL> i, a) {
287                 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
288                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
289                         ++j;
290                 }
291
292                 if (j == b.end ()) {
293                         r = false;
294                 }
295         }
296
297         return r;
298 }
299
300 void
301 DCP::add (boost::shared_ptr<CPL> cpl)
302 {
303         _cpls.push_back (cpl);
304 }
305
306 bool
307 DCP::encrypted () const
308 {
309         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
310                 if (i->encrypted ()) {
311                         return true;
312                 }
313         }
314
315         return false;
316 }
317
318 /** Add a KDM to decrypt this DCP.  This method must be called after DCP::read()
319  *  or the KDM you specify will be ignored.
320  *  @param kdm KDM to use.
321  */
322 void
323 DCP::add (DecryptedKDM const & kdm)
324 {
325         list<DecryptedKDMKey> keys = kdm.keys ();
326
327         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
328                 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
329                         if (j.cpl_id() == i->id()) {
330                                 i->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         boost::filesystem::path p = _directory;
343         switch (standard) {
344         case INTEROP:
345                 p /= "VOLINDEX";
346                 break;
347         case 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 INTEROP:
359                 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
360                 break;
361         case 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 void
373 DCP::write_assetmap (Standard standard, string pkl_uuid, boost::filesystem::path pkl_path, XMLMetadata metadata) const
374 {
375         boost::filesystem::path p = _directory;
376
377         switch (standard) {
378         case INTEROP:
379                 p /= "ASSETMAP";
380                 break;
381         case SMPTE:
382                 p /= "ASSETMAP.xml";
383                 break;
384         default:
385                 DCP_ASSERT (false);
386         }
387
388         xmlpp::Document doc;
389         xmlpp::Element* root;
390
391         switch (standard) {
392         case INTEROP:
393                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
394                 break;
395         case SMPTE:
396                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
397                 break;
398         default:
399                 DCP_ASSERT (false);
400         }
401
402         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
403         root->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
404
405         switch (standard) {
406         case INTEROP:
407                 root->add_child("VolumeCount")->add_child_text ("1");
408                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
409                 root->add_child("Issuer")->add_child_text (metadata.issuer);
410                 root->add_child("Creator")->add_child_text (metadata.creator);
411                 break;
412         case SMPTE:
413                 root->add_child("Creator")->add_child_text (metadata.creator);
414                 root->add_child("VolumeCount")->add_child_text ("1");
415                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
416                 root->add_child("Issuer")->add_child_text (metadata.issuer);
417                 break;
418         default:
419                 DCP_ASSERT (false);
420         }
421
422         xmlpp::Node* asset_list = root->add_child ("AssetList");
423
424         xmlpp::Node* asset = asset_list->add_child ("Asset");
425         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
426         asset->add_child("PackingList")->add_child_text ("true");
427         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
428         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
429         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
430         chunk->add_child("VolumeIndex")->add_child_text ("1");
431         chunk->add_child("Offset")->add_child_text ("0");
432         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
433
434         BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
435                 i->write_to_assetmap (asset_list, _directory);
436         }
437
438         doc.write_to_file_formatted (p.string (), "UTF-8");
439 }
440
441 /** Write all the XML files for this DCP.
442  *  @param standand INTEROP or SMPTE.
443  *  @param metadata Metadata to use for PKL and asset map files.
444  *  @param signer Signer to use, or 0.
445  */
446 void
447 DCP::write_xml (
448         Standard standard,
449         XMLMetadata metadata,
450         shared_ptr<const CertificateChain> signer,
451         NameFormat name_format
452         )
453 {
454         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
455                 NameFormat::Map values;
456                 values['t'] = "cpl";
457                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
458         }
459
460         shared_ptr<PKL> pkl;
461
462         if (_pkls.empty()) {
463                 pkl.reset (new PKL (standard, metadata.annotation_text, metadata.issue_date, metadata.issuer, metadata.creator));
464                 _pkls.push_back (pkl);
465                 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
466                         i->add_to_pkl (pkl, _directory);
467                 }
468         } else {
469                 pkl = _pkls.front ();
470         }
471
472         NameFormat::Map values;
473         values['t'] = "pkl";
474         boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
475         pkl->write (pkl_path, signer);
476
477         write_volindex (standard);
478         write_assetmap (standard, pkl->id(), pkl_path, metadata);
479 }
480
481 list<shared_ptr<CPL> >
482 DCP::cpls () const
483 {
484         return _cpls;
485 }
486
487 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
488  *  an exception is thrown if they are found.
489  *  @return All assets (including CPLs).
490  */
491 list<shared_ptr<Asset> >
492 DCP::assets (bool ignore_unresolved) const
493 {
494         list<shared_ptr<Asset> > assets;
495         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
496                 assets.push_back (i);
497                 BOOST_FOREACH (shared_ptr<const ReelMXF> j, i->reel_mxfs()) {
498                         if (ignore_unresolved && !j->asset_ref().resolved()) {
499                                 continue;
500                         }
501                         shared_ptr<Asset> o = j->asset_ref().asset ();
502                         assets.push_back (o);
503                         /* More Interop special-casing */
504                         shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
505                         if (sub) {
506                                 sub->add_font_assets (assets);
507                         }
508                 }
509         }
510
511         return assets;
512 }
513
514 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
515 vector<boost::filesystem::path>
516 DCP::directories_from_files (vector<boost::filesystem::path> files)
517 {
518         vector<boost::filesystem::path> d;
519         BOOST_FOREACH (boost::filesystem::path i, files) {
520                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
521                         d.push_back (i.parent_path ());
522                 }
523         }
524         return d;
525 }