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