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