Adjust for Interop <PackingList> not needing content.
[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                 }
265         }
266
267         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
268                 i->resolve_refs (other_assets);
269         }
270 }
271
272 void
273 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
274 {
275         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
276                 i->resolve_refs (assets);
277         }
278 }
279
280 bool
281 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
282 {
283         list<shared_ptr<CPL> > a = cpls ();
284         list<shared_ptr<CPL> > b = other.cpls ();
285
286         if (a.size() != b.size()) {
287                 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
288                 return false;
289         }
290
291         bool r = true;
292
293         BOOST_FOREACH (shared_ptr<CPL> i, a) {
294                 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
295                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
296                         ++j;
297                 }
298
299                 if (j == b.end ()) {
300                         r = false;
301                 }
302         }
303
304         return r;
305 }
306
307 void
308 DCP::add (boost::shared_ptr<CPL> cpl)
309 {
310         _cpls.push_back (cpl);
311 }
312
313 bool
314 DCP::encrypted () const
315 {
316         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
317                 if (i->encrypted ()) {
318                         return true;
319                 }
320         }
321
322         return false;
323 }
324
325 /** Add a KDM to decrypt this DCP.  This method must be called after DCP::read()
326  *  or the KDM you specify will be ignored.
327  *  @param kdm KDM to use.
328  */
329 void
330 DCP::add (DecryptedKDM const & kdm)
331 {
332         list<DecryptedKDMKey> keys = kdm.keys ();
333
334         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
335                 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
336                         if (j.cpl_id() == i->id()) {
337                                 i->add (kdm);
338                         }
339                 }
340         }
341 }
342
343 /** Write the VOLINDEX file.
344  *  @param standard DCP standard to use (INTEROP or SMPTE)
345  */
346 void
347 DCP::write_volindex (Standard standard) const
348 {
349         boost::filesystem::path p = _directory;
350         switch (standard) {
351         case INTEROP:
352                 p /= "VOLINDEX";
353                 break;
354         case SMPTE:
355                 p /= "VOLINDEX.xml";
356                 break;
357         default:
358                 DCP_ASSERT (false);
359         }
360
361         xmlpp::Document doc;
362         xmlpp::Element* root;
363
364         switch (standard) {
365         case INTEROP:
366                 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
367                 break;
368         case SMPTE:
369                 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
370                 break;
371         default:
372                 DCP_ASSERT (false);
373         }
374
375         root->add_child("Index")->add_child_text ("1");
376         doc.write_to_file (p.string (), "UTF-8");
377 }
378
379 void
380 DCP::write_assetmap (Standard standard, string pkl_uuid, boost::filesystem::path pkl_path, XMLMetadata metadata) const
381 {
382         boost::filesystem::path p = _directory;
383
384         switch (standard) {
385         case INTEROP:
386                 p /= "ASSETMAP";
387                 break;
388         case SMPTE:
389                 p /= "ASSETMAP.xml";
390                 break;
391         default:
392                 DCP_ASSERT (false);
393         }
394
395         xmlpp::Document doc;
396         xmlpp::Element* root;
397
398         switch (standard) {
399         case INTEROP:
400                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
401                 break;
402         case SMPTE:
403                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
404                 break;
405         default:
406                 DCP_ASSERT (false);
407         }
408
409         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
410         root->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
411
412         switch (standard) {
413         case INTEROP:
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                 root->add_child("Creator")->add_child_text (metadata.creator);
418                 break;
419         case SMPTE:
420                 root->add_child("Creator")->add_child_text (metadata.creator);
421                 root->add_child("VolumeCount")->add_child_text ("1");
422                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
423                 root->add_child("Issuer")->add_child_text (metadata.issuer);
424                 break;
425         default:
426                 DCP_ASSERT (false);
427         }
428
429         xmlpp::Node* asset_list = root->add_child ("AssetList");
430
431         xmlpp::Node* asset = asset_list->add_child ("Asset");
432         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
433         asset->add_child("PackingList")->add_child_text ("true");
434         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
435         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
436         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
437         chunk->add_child("VolumeIndex")->add_child_text ("1");
438         chunk->add_child("Offset")->add_child_text ("0");
439         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
440
441         BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
442                 i->write_to_assetmap (asset_list, _directory);
443         }
444
445         /* This must not be the _formatted version otherwise signature digests will be wrong */
446         doc.write_to_file (p.string (), "UTF-8");
447 }
448
449 /** Write all the XML files for this DCP.
450  *  @param standand INTEROP or SMPTE.
451  *  @param metadata Metadata to use for PKL and asset map files.
452  *  @param signer Signer to use, or 0.
453  */
454 void
455 DCP::write_xml (
456         Standard standard,
457         XMLMetadata metadata,
458         shared_ptr<const CertificateChain> signer,
459         NameFormat name_format
460         )
461 {
462         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
463                 NameFormat::Map values;
464                 values['t'] = "cpl";
465                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
466         }
467
468         if (!_pkl) {
469                 _pkl.reset (new PKL (standard, metadata.annotation_text, metadata.issue_date, metadata.issuer, metadata.creator));
470                 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
471                         i->add_to_pkl (_pkl, _directory);
472                 }
473         }
474
475         NameFormat::Map values;
476         values['t'] = "pkl";
477         boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + _pkl->id() + ".xml");
478         _pkl->write (pkl_path, signer);
479
480         write_volindex (standard);
481         write_assetmap (standard, _pkl->id(), pkl_path, metadata);
482 }
483
484 list<shared_ptr<CPL> >
485 DCP::cpls () const
486 {
487         return _cpls;
488 }
489
490 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
491  *  an exception is thrown if they are found.
492  *  @return All assets (including CPLs).
493  */
494 list<shared_ptr<Asset> >
495 DCP::assets (bool ignore_unresolved) const
496 {
497         list<shared_ptr<Asset> > assets;
498         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
499                 assets.push_back (i);
500                 BOOST_FOREACH (shared_ptr<const ReelAsset> j, i->reel_assets ()) {
501                         if (ignore_unresolved && !j->asset_ref().resolved()) {
502                                 continue;
503                         }
504                         shared_ptr<Asset> o = j->asset_ref().asset ();
505                         assets.push_back (o);
506                         /* More Interop special-casing */
507                         shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
508                         if (sub) {
509                                 sub->add_font_assets (assets);
510                         }
511                 }
512         }
513
514         return assets;
515 }
516
517 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
518 vector<boost::filesystem::path>
519 DCP::directories_from_files (vector<boost::filesystem::path> files)
520 {
521         vector<boost::filesystem::path> d;
522         BOOST_FOREACH (boost::filesystem::path i, files) {
523                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
524                         d.push_back (i.parent_path ());
525                 }
526         }
527         return d;
528 }