Simpole DCP recovery utility (dcprecover) added.
[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 (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         /* This must not be the _formatted version otherwise signature digests will be wrong */
421         doc.write_to_file (p.string (), "UTF-8");
422 }
423
424 /** Write all the XML files for this DCP.
425  *  @param standand INTEROP or SMPTE.
426  *  @param metadata Metadata to use for PKL and asset map files.
427  *  @param signer Signer to use, or 0.
428  */
429 void
430 DCP::write_xml (
431         Standard standard,
432         XMLMetadata metadata,
433         shared_ptr<const CertificateChain> signer,
434         NameFormat name_format
435         )
436 {
437         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
438                 NameFormat::Map values;
439                 values['t'] = "cpl";
440                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
441         }
442
443         shared_ptr<PKL> pkl;
444
445         if (_pkls.empty()) {
446                 pkl.reset (new PKL (standard, metadata.annotation_text, metadata.issue_date, metadata.issuer, metadata.creator));
447                 _pkls.push_back (pkl);
448                 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
449                         i->add_to_pkl (pkl, _directory);
450                 }
451         } else {
452                 pkl = _pkls.front ();
453         }
454
455         NameFormat::Map values;
456         values['t'] = "pkl";
457         boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
458         pkl->write (pkl_path, signer);
459
460         write_volindex (standard);
461         write_assetmap (standard, pkl->id(), pkl_path, metadata);
462 }
463
464 list<shared_ptr<CPL> >
465 DCP::cpls () const
466 {
467         return _cpls;
468 }
469
470 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
471  *  an exception is thrown if they are found.
472  *  @return All assets (including CPLs).
473  */
474 list<shared_ptr<Asset> >
475 DCP::assets (bool ignore_unresolved) const
476 {
477         list<shared_ptr<Asset> > assets;
478         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
479                 assets.push_back (i);
480                 BOOST_FOREACH (shared_ptr<const ReelAsset> j, i->reel_assets ()) {
481                         if (ignore_unresolved && !j->asset_ref().resolved()) {
482                                 continue;
483                         }
484                         shared_ptr<Asset> o = j->asset_ref().asset ();
485                         assets.push_back (o);
486                         /* More Interop special-casing */
487                         shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
488                         if (sub) {
489                                 sub->add_font_assets (assets);
490                         }
491                 }
492         }
493
494         return assets;
495 }
496
497 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
498 vector<boost::filesystem::path>
499 DCP::directories_from_files (vector<boost::filesystem::path> files)
500 {
501         vector<boost::filesystem::path> d;
502         BOOST_FOREACH (boost::filesystem::path i, files) {
503                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
504                         d.push_back (i.parent_path ());
505                 }
506         }
507         return d;
508 }