Result is specific to verify_asset().
[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 "verify.h"
62 #include <asdcp/AS_DCP.h>
63 #include <xmlsec/xmldsig.h>
64 #include <xmlsec/app.h>
65 #include <libxml++/libxml++.h>
66 #include <boost/filesystem.hpp>
67 #include <boost/algorithm/string.hpp>
68 #include <boost/foreach.hpp>
69
70 using std::string;
71 using std::list;
72 using std::vector;
73 using std::cout;
74 using std::make_pair;
75 using std::map;
76 using std::cerr;
77 using std::exception;
78 using boost::shared_ptr;
79 using boost::dynamic_pointer_cast;
80 using boost::optional;
81 using boost::algorithm::starts_with;
82 using namespace dcp;
83
84 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
85 static string const assetmap_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
86 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
87 static string const volindex_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
88
89 DCP::DCP (boost::filesystem::path directory)
90         : _directory (directory)
91 {
92         if (!boost::filesystem::exists (directory)) {
93                 boost::filesystem::create_directories (directory);
94         }
95
96         _directory = boost::filesystem::canonical (_directory);
97 }
98
99 /** Read a DCP.  This method does not do any deep checking of the DCP's validity, but
100  *  if it comes across any bad things it will do one of two things.
101  *
102  *  Errors that are so serious that they prevent the method from working will result
103  *  in an exception being thrown.  For example, a missing ASSETMAP means that the DCP
104  *  can't be read without a lot of guesswork, so this will throw.
105  *
106  *  Errors that are not fatal will be added to notes, if it's non-0.  For example,
107  *  if the DCP contains a mixture of Interop and SMPTE elements this will result
108  *  in a note being added to the list.
109  */
110 void
111 DCP::read (list<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
112 {
113         /* Read the ASSETMAP and PKL */
114
115         if (boost::filesystem::exists (_directory / "ASSETMAP")) {
116                 _asset_map = _directory / "ASSETMAP";
117         } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
118                 _asset_map = _directory / "ASSETMAP.xml";
119         } else {
120                 boost::throw_exception (ReadError (String::compose ("could not find ASSETMAP nor ASSETMAP.xml in `%1'", _directory.string())));
121         }
122
123         cxml::Document asset_map ("AssetMap");
124
125         asset_map.read_file (_asset_map.get());
126         if (asset_map.namespace_uri() == assetmap_interop_ns) {
127                 _standard = INTEROP;
128         } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
129                 _standard = SMPTE;
130         } else {
131                 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
132         }
133
134         list<shared_ptr<cxml::Node> > asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
135         map<string, boost::filesystem::path> paths;
136         list<boost::filesystem::path> pkl_paths;
137         BOOST_FOREACH (shared_ptr<cxml::Node> i, asset_nodes) {
138                 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
139                         boost::throw_exception (XMLError ("unsupported asset chunk count"));
140                 }
141                 string p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
142                 if (starts_with (p, "file://")) {
143                         p = p.substr (7);
144                 }
145                 switch (*_standard) {
146                 case INTEROP:
147                         if (i->optional_node_child("PackingList")) {
148                                 pkl_paths.push_back (p);
149                         } else {
150                                 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
151                         }
152                         break;
153                 case SMPTE:
154                 {
155                         optional<string> pkl_bool = i->optional_string_child("PackingList");
156                         if (pkl_bool && *pkl_bool == "true") {
157                                 pkl_paths.push_back (p);
158                         } else {
159                                 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
160                         }
161                         break;
162                 }
163                 }
164         }
165
166         if (pkl_paths.empty()) {
167                 boost::throw_exception (XMLError ("No packing lists found in asset map"));
168         }
169
170         BOOST_FOREACH (boost::filesystem::path i, pkl_paths) {
171                 _pkls.push_back (shared_ptr<PKL>(new PKL(_directory / i)));
172         }
173
174         /* Now we have:
175              paths - files in the DCP that are not PKLs.
176              _pkls - PKL objects for each PKL.
177
178            Read all the assets from the asset map.
179          */
180
181         /* Make a list of non-CPL/PKL assets so that we can resolve the references
182            from the CPLs.
183         */
184         list<shared_ptr<Asset> > other_assets;
185
186         for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
187                 boost::filesystem::path path = _directory / i->second;
188
189                 if (i->second.empty()) {
190                         /* I can't see how this is valid, but it's
191                            been seen in the wild with a DCP that
192                            claims to come from ClipsterDCI 5.10.0.5.
193                         */
194                         if (notes) {
195                                 notes->push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::EMPTY_ASSET_PATH));
196                         }
197                         continue;
198                 }
199
200                 if (!boost::filesystem::exists(path)) {
201                         if (notes) {
202                                 notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISSING_ASSET, path));
203                         }
204                         continue;
205                 }
206
207                 /* Find the <Type> for this asset from the PKL that contains the asset */
208                 optional<string> pkl_type;
209                 BOOST_FOREACH (shared_ptr<PKL> j, _pkls) {
210                         pkl_type = j->type(i->first);
211                         if (pkl_type) {
212                                 break;
213                         }
214                 }
215
216                 DCP_ASSERT (pkl_type);
217
218                 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
219                         xmlpp::DomParser* p = new xmlpp::DomParser;
220                         try {
221                                 p->parse_file (path.string());
222                         } catch (std::exception& e) {
223                                 delete p;
224                                 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
225                         }
226
227                         string const root = p->get_document()->get_root_node()->get_name ();
228                         delete p;
229
230                         if (root == "CompositionPlaylist") {
231                                 shared_ptr<CPL> cpl (new CPL (path));
232                                 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get() && notes) {
233                                         notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
234                                 }
235                                 _cpls.push_back (cpl);
236                         } else if (root == "DCSubtitle") {
237                                 if (_standard && _standard.get() == SMPTE) {
238                                         notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
239                                 }
240                                 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
241                         }
242                 } else if (
243                         *pkl_type == PictureAsset::static_pkl_type(*_standard) ||
244                         *pkl_type == SoundAsset::static_pkl_type(*_standard) ||
245                         *pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
246                         *pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
247                         ) {
248
249                         other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type));
250                 } else if (*pkl_type == FontAsset::static_pkl_type(*_standard)) {
251                         other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
252                 } else if (*pkl_type == "image/png") {
253                         /* It's an Interop PNG subtitle; let it go */
254                 } else {
255                         throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
256                 }
257         }
258
259         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
260                 i->resolve_refs (other_assets);
261         }
262 }
263
264 void
265 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
266 {
267         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
268                 i->resolve_refs (assets);
269         }
270 }
271
272 bool
273 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
274 {
275         list<shared_ptr<CPL> > a = cpls ();
276         list<shared_ptr<CPL> > b = other.cpls ();
277
278         if (a.size() != b.size()) {
279                 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
280                 return false;
281         }
282
283         bool r = true;
284
285         BOOST_FOREACH (shared_ptr<CPL> i, a) {
286                 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
287                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
288                         ++j;
289                 }
290
291                 if (j == b.end ()) {
292                         r = false;
293                 }
294         }
295
296         return r;
297 }
298
299 void
300 DCP::add (boost::shared_ptr<CPL> cpl)
301 {
302         _cpls.push_back (cpl);
303 }
304
305 bool
306 DCP::encrypted () const
307 {
308         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
309                 if (i->encrypted ()) {
310                         return true;
311                 }
312         }
313
314         return false;
315 }
316
317 /** Add a KDM to decrypt this DCP.  This method must be called after DCP::read()
318  *  or the KDM you specify will be ignored.
319  *  @param kdm KDM to use.
320  */
321 void
322 DCP::add (DecryptedKDM const & kdm)
323 {
324         list<DecryptedKDMKey> keys = kdm.keys ();
325
326         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
327                 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
328                         if (j.cpl_id() == i->id()) {
329                                 i->add (kdm);
330                         }
331                 }
332         }
333 }
334
335 /** Write the VOLINDEX file.
336  *  @param standard DCP standard to use (INTEROP or SMPTE)
337  */
338 void
339 DCP::write_volindex (Standard standard) const
340 {
341         boost::filesystem::path p = _directory;
342         switch (standard) {
343         case INTEROP:
344                 p /= "VOLINDEX";
345                 break;
346         case SMPTE:
347                 p /= "VOLINDEX.xml";
348                 break;
349         default:
350                 DCP_ASSERT (false);
351         }
352
353         xmlpp::Document doc;
354         xmlpp::Element* root;
355
356         switch (standard) {
357         case INTEROP:
358                 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
359                 break;
360         case SMPTE:
361                 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
362                 break;
363         default:
364                 DCP_ASSERT (false);
365         }
366
367         root->add_child("Index")->add_child_text ("1");
368         doc.write_to_file_formatted (p.string (), "UTF-8");
369 }
370
371 void
372 DCP::write_assetmap (Standard standard, string pkl_uuid, boost::filesystem::path pkl_path, XMLMetadata metadata) const
373 {
374         boost::filesystem::path p = _directory;
375
376         switch (standard) {
377         case INTEROP:
378                 p /= "ASSETMAP";
379                 break;
380         case SMPTE:
381                 p /= "ASSETMAP.xml";
382                 break;
383         default:
384                 DCP_ASSERT (false);
385         }
386
387         xmlpp::Document doc;
388         xmlpp::Element* root;
389
390         switch (standard) {
391         case INTEROP:
392                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
393                 break;
394         case SMPTE:
395                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
396                 break;
397         default:
398                 DCP_ASSERT (false);
399         }
400
401         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
402         root->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
403
404         switch (standard) {
405         case INTEROP:
406                 root->add_child("VolumeCount")->add_child_text ("1");
407                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
408                 root->add_child("Issuer")->add_child_text (metadata.issuer);
409                 root->add_child("Creator")->add_child_text (metadata.creator);
410                 break;
411         case SMPTE:
412                 root->add_child("Creator")->add_child_text (metadata.creator);
413                 root->add_child("VolumeCount")->add_child_text ("1");
414                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
415                 root->add_child("Issuer")->add_child_text (metadata.issuer);
416                 break;
417         default:
418                 DCP_ASSERT (false);
419         }
420
421         xmlpp::Node* asset_list = root->add_child ("AssetList");
422
423         xmlpp::Node* asset = asset_list->add_child ("Asset");
424         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
425         asset->add_child("PackingList")->add_child_text ("true");
426         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
427         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
428         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
429         chunk->add_child("VolumeIndex")->add_child_text ("1");
430         chunk->add_child("Offset")->add_child_text ("0");
431         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
432
433         BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
434                 i->write_to_assetmap (asset_list, _directory);
435         }
436
437         doc.write_to_file_formatted (p.string (), "UTF-8");
438         _asset_map = p;
439 }
440
441 /** Write all the XML files for this DCP.
442  *  @param standand INTEROP or SMPTE.
443  *  @param metadata Metadata to use for PKL and asset map files.
444  *  @param signer Signer to use, or 0.
445  */
446 void
447 DCP::write_xml (
448         Standard standard,
449         XMLMetadata metadata,
450         shared_ptr<const CertificateChain> signer,
451         NameFormat name_format
452         )
453 {
454         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
455                 NameFormat::Map values;
456                 values['t'] = "cpl";
457                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
458         }
459
460         shared_ptr<PKL> pkl;
461
462         if (_pkls.empty()) {
463                 pkl.reset (new PKL (standard, metadata.annotation_text, metadata.issue_date, metadata.issuer, metadata.creator));
464                 _pkls.push_back (pkl);
465                 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
466                         i->add_to_pkl (pkl, _directory);
467                 }
468         } else {
469                 pkl = _pkls.front ();
470         }
471
472         NameFormat::Map values;
473         values['t'] = "pkl";
474         boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
475         pkl->write (pkl_path, signer);
476
477         write_volindex (standard);
478         write_assetmap (standard, pkl->id(), pkl_path, metadata);
479 }
480
481 list<shared_ptr<CPL> >
482 DCP::cpls () const
483 {
484         return _cpls;
485 }
486
487 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
488  *  an exception is thrown if they are found.
489  *  @return All assets (including CPLs).
490  */
491 list<shared_ptr<Asset> >
492 DCP::assets (bool ignore_unresolved) const
493 {
494         list<shared_ptr<Asset> > assets;
495         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
496                 assets.push_back (i);
497                 BOOST_FOREACH (shared_ptr<const ReelMXF> j, i->reel_mxfs()) {
498                         if (ignore_unresolved && !j->asset_ref().resolved()) {
499                                 continue;
500                         }
501                         shared_ptr<Asset> o = j->asset_ref().asset ();
502                         assets.push_back (o);
503                         /* More Interop special-casing */
504                         shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
505                         if (sub) {
506                                 sub->add_font_assets (assets);
507                         }
508                 }
509         }
510
511         return assets;
512 }
513
514 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
515 vector<boost::filesystem::path>
516 DCP::directories_from_files (vector<boost::filesystem::path> files)
517 {
518         vector<boost::filesystem::path> d;
519         BOOST_FOREACH (boost::filesystem::path i, files) {
520                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
521                         d.push_back (i.parent_path ());
522                 }
523         }
524         return d;
525 }