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