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