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