Fix erroneous reports of unresolved assets when checking OV/VF pairs.
[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 <asdcp/AS_DCP.h>
62 #include <xmlsec/xmldsig.h>
63 #include <xmlsec/app.h>
64 #include <libxml++/libxml++.h>
65 #include <boost/filesystem.hpp>
66 #include <boost/algorithm/string.hpp>
67 #include <boost/foreach.hpp>
68
69 using std::string;
70 using std::list;
71 using std::vector;
72 using std::cout;
73 using std::make_pair;
74 using std::map;
75 using std::cerr;
76 using std::exception;
77 using boost::shared_ptr;
78 using boost::dynamic_pointer_cast;
79 using boost::optional;
80 using boost::algorithm::starts_with;
81 using namespace dcp;
82
83 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
84 static string const assetmap_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
85 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
86 static string const volindex_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
87
88 DCP::DCP (boost::filesystem::path directory)
89         : _directory (directory)
90 {
91         if (!boost::filesystem::exists (directory)) {
92                 boost::filesystem::create_directories (directory);
93         }
94
95         _directory = boost::filesystem::canonical (_directory);
96 }
97
98 /** Call this instead of throwing an exception if the error can be tolerated */
99 template<class T> void
100 survivable_error (bool keep_going, dcp::DCP::ReadErrors* errors, T const & e)
101 {
102         if (keep_going) {
103                 if (errors) {
104                         errors->push_back (shared_ptr<T> (new T (e)));
105                 }
106         } else {
107                 throw e;
108         }
109 }
110
111 void
112 DCP::read (bool keep_going, ReadErrors* errors, bool ignore_incorrect_picture_mxf_type)
113 {
114         /* Read the ASSETMAP and PKL */
115
116         boost::filesystem::path asset_map_file;
117         if (boost::filesystem::exists (_directory / "ASSETMAP")) {
118                 asset_map_file = _directory / "ASSETMAP";
119         } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
120                 asset_map_file = _directory / "ASSETMAP.xml";
121         } else {
122                 boost::throw_exception (DCPReadError (String::compose ("could not find AssetMap file in `%1'", _directory.string())));
123         }
124
125         cxml::Document asset_map ("AssetMap");
126
127         asset_map.read_file (asset_map_file);
128         if (asset_map.namespace_uri() == assetmap_interop_ns) {
129                 _standard = INTEROP;
130         } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
131                 _standard = SMPTE;
132         } else {
133                 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
134         }
135
136         list<shared_ptr<cxml::Node> > asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
137         map<string, boost::filesystem::path> paths;
138         list<boost::filesystem::path> pkl_paths;
139         BOOST_FOREACH (shared_ptr<cxml::Node> i, asset_nodes) {
140                 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
141                         boost::throw_exception (XMLError ("unsupported asset chunk count"));
142                 }
143                 string p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
144                 if (starts_with (p, "file://")) {
145                         p = p.substr (7);
146                 }
147                 switch (*_standard) {
148                 case INTEROP:
149                         if (i->optional_node_child("PackingList")) {
150                                 pkl_paths.push_back (p);
151                         } else {
152                                 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
153                         }
154                         break;
155                 case SMPTE:
156                 {
157                         optional<string> pkl_bool = i->optional_string_child("PackingList");
158                         if (pkl_bool && *pkl_bool == "true") {
159                                 pkl_paths.push_back (p);
160                         } else {
161                                 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
162                         }
163                         break;
164                 }
165                 }
166         }
167
168         if (pkl_paths.empty()) {
169                 boost::throw_exception (XMLError ("No packing lists found in asset map"));
170         }
171
172         BOOST_FOREACH (boost::filesystem::path i, pkl_paths) {
173                 _pkls.push_back (shared_ptr<PKL>(new PKL(_directory / i)));
174         }
175
176         /* Now we have:
177              paths - files in the DCP that are not PKLs.
178              _pkls - PKL objects for each PKL.
179
180            Read all the assets from the asset map.
181          */
182
183         /* Make a list of non-CPL/PKL assets so that we can resolve the references
184            from the CPLs.
185         */
186         list<shared_ptr<Asset> > other_assets;
187
188         for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
189                 boost::filesystem::path path = _directory / i->second;
190
191                 if (i->second.empty()) {
192                         /* I can't see how this is valid, but it's
193                            been seen in the wild with a DCP that
194                            claims to come from ClipsterDCI 5.10.0.5.
195                         */
196                         survivable_error (keep_going, errors, EmptyAssetPathError(i->first));
197                         continue;
198                 }
199
200                 if (!boost::filesystem::exists(path)) {
201                         survivable_error (keep_going, errors, MissingAssetError (path));
202                         continue;
203                 }
204
205                 /* Find the <Type> for this asset from the PKL that contains the asset */
206                 optional<string> pkl_type;
207                 BOOST_FOREACH (shared_ptr<PKL> j, _pkls) {
208                         pkl_type = j->type(i->first);
209                         if (pkl_type) {
210                                 break;
211                         }
212                 }
213
214                 if (!pkl_type) {
215                         /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
216                          * need to worry about it.
217                          */
218                         continue;
219                 }
220
221                 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
222                         xmlpp::DomParser* p = new xmlpp::DomParser;
223                         try {
224                                 p->parse_file (path.string());
225                         } catch (std::exception& e) {
226                                 delete p;
227                                 throw DCPReadError(String::compose("XML error in %1", path.string()), e.what());
228                         }
229
230                         string const root = p->get_document()->get_root_node()->get_name ();
231                         delete p;
232
233                         if (root == "CompositionPlaylist") {
234                                 shared_ptr<CPL> cpl (new CPL (path));
235                                 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get()) {
236                                         survivable_error (keep_going, errors, MismatchedStandardError ());
237                                 }
238                                 _cpls.push_back (cpl);
239                         } else if (root == "DCSubtitle") {
240                                 if (_standard && _standard.get() == SMPTE) {
241                                         survivable_error (keep_going, errors, MismatchedStandardError ());
242                                 }
243                                 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
244                         }
245                 } else if (
246                         *pkl_type == PictureAsset::static_pkl_type(*_standard) ||
247                         *pkl_type == SoundAsset::static_pkl_type(*_standard) ||
248                         *pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
249                         *pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
250                         ) {
251
252                         other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type));
253                 } else if (*pkl_type == FontAsset::static_pkl_type(*_standard)) {
254                         other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
255                 } else if (*pkl_type == "image/png") {
256                         /* It's an Interop PNG subtitle; let it go */
257                 } else {
258                         throw DCPReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
259                 }
260         }
261
262         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
263                 i->resolve_refs (other_assets);
264         }
265 }
266
267 void
268 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
269 {
270         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
271                 i->resolve_refs (assets);
272         }
273 }
274
275 bool
276 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
277 {
278         list<shared_ptr<CPL> > a = cpls ();
279         list<shared_ptr<CPL> > b = other.cpls ();
280
281         if (a.size() != b.size()) {
282                 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
283                 return false;
284         }
285
286         bool r = true;
287
288         BOOST_FOREACH (shared_ptr<CPL> i, a) {
289                 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
290                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
291                         ++j;
292                 }
293
294                 if (j == b.end ()) {
295                         r = false;
296                 }
297         }
298
299         return r;
300 }
301
302 void
303 DCP::add (boost::shared_ptr<CPL> cpl)
304 {
305         _cpls.push_back (cpl);
306 }
307
308 bool
309 DCP::encrypted () const
310 {
311         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
312                 if (i->encrypted ()) {
313                         return true;
314                 }
315         }
316
317         return false;
318 }
319
320 /** Add a KDM to decrypt this DCP.  This method must be called after DCP::read()
321  *  or the KDM you specify will be ignored.
322  *  @param kdm KDM to use.
323  */
324 void
325 DCP::add (DecryptedKDM const & kdm)
326 {
327         list<DecryptedKDMKey> keys = kdm.keys ();
328
329         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
330                 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
331                         if (j.cpl_id() == i->id()) {
332                                 i->add (kdm);
333                         }
334                 }
335         }
336 }
337
338 /** Write the VOLINDEX file.
339  *  @param standard DCP standard to use (INTEROP or SMPTE)
340  */
341 void
342 DCP::write_volindex (Standard standard) const
343 {
344         boost::filesystem::path p = _directory;
345         switch (standard) {
346         case INTEROP:
347                 p /= "VOLINDEX";
348                 break;
349         case SMPTE:
350                 p /= "VOLINDEX.xml";
351                 break;
352         default:
353                 DCP_ASSERT (false);
354         }
355
356         xmlpp::Document doc;
357         xmlpp::Element* root;
358
359         switch (standard) {
360         case INTEROP:
361                 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
362                 break;
363         case SMPTE:
364                 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
365                 break;
366         default:
367                 DCP_ASSERT (false);
368         }
369
370         root->add_child("Index")->add_child_text ("1");
371         doc.write_to_file_formatted (p.string (), "UTF-8");
372 }
373
374 void
375 DCP::write_assetmap (Standard standard, string pkl_uuid, boost::filesystem::path pkl_path, XMLMetadata metadata) const
376 {
377         boost::filesystem::path p = _directory;
378
379         switch (standard) {
380         case INTEROP:
381                 p /= "ASSETMAP";
382                 break;
383         case SMPTE:
384                 p /= "ASSETMAP.xml";
385                 break;
386         default:
387                 DCP_ASSERT (false);
388         }
389
390         xmlpp::Document doc;
391         xmlpp::Element* root;
392
393         switch (standard) {
394         case INTEROP:
395                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
396                 break;
397         case SMPTE:
398                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
399                 break;
400         default:
401                 DCP_ASSERT (false);
402         }
403
404         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
405         root->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
406
407         switch (standard) {
408         case INTEROP:
409                 root->add_child("VolumeCount")->add_child_text ("1");
410                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
411                 root->add_child("Issuer")->add_child_text (metadata.issuer);
412                 root->add_child("Creator")->add_child_text (metadata.creator);
413                 break;
414         case SMPTE:
415                 root->add_child("Creator")->add_child_text (metadata.creator);
416                 root->add_child("VolumeCount")->add_child_text ("1");
417                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
418                 root->add_child("Issuer")->add_child_text (metadata.issuer);
419                 break;
420         default:
421                 DCP_ASSERT (false);
422         }
423
424         xmlpp::Node* asset_list = root->add_child ("AssetList");
425
426         xmlpp::Node* asset = asset_list->add_child ("Asset");
427         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
428         asset->add_child("PackingList")->add_child_text ("true");
429         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
430         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
431         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
432         chunk->add_child("VolumeIndex")->add_child_text ("1");
433         chunk->add_child("Offset")->add_child_text ("0");
434         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
435
436         BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
437                 i->write_to_assetmap (asset_list, _directory);
438         }
439
440         doc.write_to_file_formatted (p.string (), "UTF-8");
441 }
442
443 /** Write all the XML files for this DCP.
444  *  @param standand INTEROP or SMPTE.
445  *  @param metadata Metadata to use for PKL and asset map files.
446  *  @param signer Signer to use, or 0.
447  */
448 void
449 DCP::write_xml (
450         Standard standard,
451         XMLMetadata metadata,
452         shared_ptr<const CertificateChain> signer,
453         NameFormat name_format
454         )
455 {
456         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
457                 NameFormat::Map values;
458                 values['t'] = "cpl";
459                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
460         }
461
462         shared_ptr<PKL> pkl;
463
464         if (_pkls.empty()) {
465                 pkl.reset (new PKL (standard, metadata.annotation_text, metadata.issue_date, metadata.issuer, metadata.creator));
466                 _pkls.push_back (pkl);
467                 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
468                         i->add_to_pkl (pkl, _directory);
469                 }
470         } else {
471                 pkl = _pkls.front ();
472         }
473
474         NameFormat::Map values;
475         values['t'] = "pkl";
476         boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
477         pkl->write (pkl_path, signer);
478
479         write_volindex (standard);
480         write_assetmap (standard, pkl->id(), pkl_path, metadata);
481 }
482
483 list<shared_ptr<CPL> >
484 DCP::cpls () const
485 {
486         return _cpls;
487 }
488
489 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
490  *  an exception is thrown if they are found.
491  *  @return All assets (including CPLs).
492  */
493 list<shared_ptr<Asset> >
494 DCP::assets (bool ignore_unresolved) const
495 {
496         list<shared_ptr<Asset> > assets;
497         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
498                 assets.push_back (i);
499                 BOOST_FOREACH (shared_ptr<const ReelAsset> j, i->reel_assets ()) {
500                         if (ignore_unresolved && !j->asset_ref().resolved()) {
501                                 continue;
502                         }
503                         shared_ptr<Asset> o = j->asset_ref().asset ();
504                         assets.push_back (o);
505                         /* More Interop special-casing */
506                         shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
507                         if (sub) {
508                                 sub->add_font_assets (assets);
509                         }
510                 }
511         }
512
513         return assets;
514 }
515
516 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
517 vector<boost::filesystem::path>
518 DCP::directories_from_files (vector<boost::filesystem::path> files)
519 {
520         vector<boost::filesystem::path> d;
521         BOOST_FOREACH (boost::filesystem::path i, files) {
522                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
523                         d.push_back (i.parent_path ());
524                 }
525         }
526         return d;
527 }