Add comment.
[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 <asdcp/AS_DCP.h>
60 #include <xmlsec/xmldsig.h>
61 #include <xmlsec/app.h>
62 #include <libxml++/libxml++.h>
63 #include <boost/filesystem.hpp>
64 #include <boost/algorithm/string.hpp>
65 #include <boost/foreach.hpp>
66
67 using std::string;
68 using std::list;
69 using std::vector;
70 using std::cout;
71 using std::make_pair;
72 using std::map;
73 using std::cerr;
74 using std::exception;
75 using boost::shared_ptr;
76 using boost::dynamic_pointer_cast;
77 using boost::algorithm::starts_with;
78 using namespace dcp;
79
80 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
81 static string const assetmap_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
82 static string const pkl_interop_ns      = "http://www.digicine.com/PROTO-ASDCP-PKL-20040311#";
83 static string const pkl_smpte_ns        = "http://www.smpte-ra.org/schemas/429-8/2007/PKL";
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 */
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         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                 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
146         }
147
148         /* Read all the assets from the asset map */
149         /* XXX: I think we should be looking at the PKL here to decide type, not
150            the extension of the file.
151         */
152
153         /* Make a list of non-CPL assets so that we can resolve the references
154            from the CPLs.
155         */
156         list<shared_ptr<Asset> > other_assets;
157
158         for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
159                 boost::filesystem::path path = _directory / i->second;
160
161                 if (!boost::filesystem::exists (path)) {
162                         survivable_error (keep_going, errors, MissingAssetError (path));
163                         continue;
164                 }
165
166                 if (boost::filesystem::extension (path) == ".xml") {
167                         xmlpp::DomParser* p = new xmlpp::DomParser;
168                         try {
169                                 p->parse_file (path.string());
170                         } catch (std::exception& e) {
171                                 delete p;
172                                 throw DCPReadError(String::compose("XML error in %1", path.string()), e.what());
173                         }
174
175                         string const root = p->get_document()->get_root_node()->get_name ();
176                         delete p;
177
178                         if (root == "CompositionPlaylist") {
179                                 shared_ptr<CPL> cpl (new CPL (path));
180                                 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get()) {
181                                         survivable_error (keep_going, errors, MismatchedStandardError ());
182                                 }
183                                 _cpls.push_back (cpl);
184                         } else if (root == "DCSubtitle") {
185                                 if (_standard && _standard.get() == SMPTE) {
186                                         survivable_error (keep_going, errors, MismatchedStandardError ());
187                                 }
188                                 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
189                         }
190                 } else if (boost::filesystem::extension (path) == ".mxf") {
191
192                         /* XXX: asdcplib does not appear to support discovery of read MXFs standard
193                            (Interop / SMPTE)
194                         */
195
196                         ASDCP::EssenceType_t type;
197                         if (ASDCP::EssenceType (path.string().c_str(), type) != ASDCP::RESULT_OK) {
198                                 throw DCPReadError ("Could not find essence type");
199                         }
200                         switch (type) {
201                                 case ASDCP::ESS_UNKNOWN:
202                                 case ASDCP::ESS_MPEG2_VES:
203                                         throw DCPReadError ("MPEG2 video essences are not supported");
204                                 case ASDCP::ESS_JPEG_2000:
205                                         try {
206                                                 other_assets.push_back (shared_ptr<MonoPictureAsset> (new MonoPictureAsset (path)));
207                                         } catch (dcp::MXFFileError& e) {
208                                                 if (ignore_incorrect_picture_mxf_type && e.number() == ASDCP::RESULT_SFORMAT) {
209                                                         /* Tried to load it as mono but the error says it's stereo; try that instead */
210                                                         other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
211                                                 } else {
212                                                         throw;
213                                                 }
214                                         }
215                                         break;
216                                 case ASDCP::ESS_PCM_24b_48k:
217                                 case ASDCP::ESS_PCM_24b_96k:
218                                         other_assets.push_back (shared_ptr<SoundAsset> (new SoundAsset (path)));
219                                         break;
220                                 case ASDCP::ESS_JPEG_2000_S:
221                                         other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
222                                         break;
223                                 case ASDCP::ESS_TIMED_TEXT:
224                                         other_assets.push_back (shared_ptr<SMPTESubtitleAsset> (new SMPTESubtitleAsset (path)));
225                                         break;
226                                 case ASDCP::ESS_DCDATA_DOLBY_ATMOS:
227                                         other_assets.push_back (shared_ptr<AtmosAsset> (new AtmosAsset (path)));
228                                         break;
229                                 default:
230                                         throw DCPReadError (String::compose ("Unknown MXF essence type %1 in %2", int(type), path.string()));
231                         }
232                 } else if (boost::filesystem::extension (path) == ".ttf") {
233                         other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
234                 }
235         }
236
237         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
238                 i->resolve_refs (other_assets);
239         }
240 }
241
242 void
243 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
244 {
245         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
246                 i->resolve_refs (assets);
247         }
248 }
249
250 bool
251 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
252 {
253         list<shared_ptr<CPL> > a = cpls ();
254         list<shared_ptr<CPL> > b = other.cpls ();
255
256         if (a.size() != b.size()) {
257                 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
258                 return false;
259         }
260
261         bool r = true;
262
263         BOOST_FOREACH (shared_ptr<CPL> i, a) {
264                 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
265                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
266                         ++j;
267                 }
268
269                 if (j == b.end ()) {
270                         r = false;
271                 }
272         }
273
274         return r;
275 }
276
277 void
278 DCP::add (boost::shared_ptr<CPL> cpl)
279 {
280         _cpls.push_back (cpl);
281 }
282
283 bool
284 DCP::encrypted () const
285 {
286         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
287                 if (i->encrypted ()) {
288                         return true;
289                 }
290         }
291
292         return false;
293 }
294
295 /** Add a KDM to decrypt this DCP.  This method must be called after DCP::read()
296  *  or the KDM you specify will be ignored.
297  *  @param kdm KDM to use.
298  */
299 void
300 DCP::add (DecryptedKDM const & kdm)
301 {
302         list<DecryptedKDMKey> keys = kdm.keys ();
303
304         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
305                 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
306                         if (j.cpl_id() == i->id()) {
307                                 i->add (kdm);
308                         }
309                 }
310         }
311 }
312
313 /** @return full pathname of PKL file that was written */
314 boost::filesystem::path
315 DCP::write_pkl (string file, Standard standard, string pkl_uuid, XMLMetadata metadata, shared_ptr<const CertificateChain> signer) const
316 {
317         boost::filesystem::path p = _directory;
318         p /= file;
319
320         xmlpp::Document doc;
321         xmlpp::Element* pkl;
322         if (standard == INTEROP) {
323                 pkl = doc.create_root_node("PackingList", pkl_interop_ns);
324         } else {
325                 pkl = doc.create_root_node("PackingList", pkl_smpte_ns);
326         }
327
328         pkl->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
329         pkl->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
330         pkl->add_child("IssueDate")->add_child_text (metadata.issue_date);
331         pkl->add_child("Issuer")->add_child_text (metadata.issuer);
332         pkl->add_child("Creator")->add_child_text (metadata.creator);
333
334         xmlpp::Element* asset_list = pkl->add_child("AssetList");
335         BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
336                 i->write_to_pkl (asset_list, _directory, standard);
337         }
338
339         if (signer) {
340                 signer->sign (pkl, standard);
341         }
342
343         doc.write_to_file (p.string (), "UTF-8");
344         return p.string ();
345 }
346
347 /** Write the VOLINDEX file.
348  *  @param standard DCP standard to use (INTEROP or SMPTE)
349  */
350 void
351 DCP::write_volindex (Standard standard) const
352 {
353         boost::filesystem::path p = _directory;
354         switch (standard) {
355         case INTEROP:
356                 p /= "VOLINDEX";
357                 break;
358         case SMPTE:
359                 p /= "VOLINDEX.xml";
360                 break;
361         default:
362                 DCP_ASSERT (false);
363         }
364
365         xmlpp::Document doc;
366         xmlpp::Element* root;
367
368         switch (standard) {
369         case INTEROP:
370                 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
371                 break;
372         case SMPTE:
373                 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
374                 break;
375         default:
376                 DCP_ASSERT (false);
377         }
378
379         root->add_child("Index")->add_child_text ("1");
380         doc.write_to_file (p.string (), "UTF-8");
381 }
382
383 void
384 DCP::write_assetmap (Standard standard, string pkl_uuid, boost::filesystem::path pkl_path, XMLMetadata metadata) const
385 {
386         boost::filesystem::path p = _directory;
387
388         switch (standard) {
389         case INTEROP:
390                 p /= "ASSETMAP";
391                 break;
392         case SMPTE:
393                 p /= "ASSETMAP.xml";
394                 break;
395         default:
396                 DCP_ASSERT (false);
397         }
398
399         xmlpp::Document doc;
400         xmlpp::Element* root;
401
402         switch (standard) {
403         case INTEROP:
404                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
405                 break;
406         case SMPTE:
407                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
408                 break;
409         default:
410                 DCP_ASSERT (false);
411         }
412
413         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
414         root->add_child("AnnotationText")->add_child_text (metadata.annotation_text);
415
416         switch (standard) {
417         case INTEROP:
418                 root->add_child("VolumeCount")->add_child_text ("1");
419                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
420                 root->add_child("Issuer")->add_child_text (metadata.issuer);
421                 root->add_child("Creator")->add_child_text (metadata.creator);
422                 break;
423         case SMPTE:
424                 root->add_child("Creator")->add_child_text (metadata.creator);
425                 root->add_child("VolumeCount")->add_child_text ("1");
426                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
427                 root->add_child("Issuer")->add_child_text (metadata.issuer);
428                 break;
429         default:
430                 DCP_ASSERT (false);
431         }
432
433         xmlpp::Node* asset_list = root->add_child ("AssetList");
434
435         xmlpp::Node* asset = asset_list->add_child ("Asset");
436         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
437         asset->add_child("PackingList")->add_child_text ("true");
438         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
439         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
440         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
441         chunk->add_child("VolumeIndex")->add_child_text ("1");
442         chunk->add_child("Offset")->add_child_text ("0");
443         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
444
445         BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
446                 i->write_to_assetmap (asset_list, _directory);
447         }
448
449         /* This must not be the _formatted version otherwise signature digests will be wrong */
450         doc.write_to_file (p.string (), "UTF-8");
451 }
452
453 /** Write all the XML files for this DCP.
454  *  @param standand INTEROP or SMPTE.
455  *  @param metadata Metadata to use for PKL and asset map files.
456  *  @param signer Signer to use, or 0.
457  */
458 void
459 DCP::write_xml (
460         Standard standard,
461         XMLMetadata metadata,
462         shared_ptr<const CertificateChain> signer,
463         NameFormat name_format
464         )
465 {
466         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
467                 NameFormat::Map values;
468                 values['t'] = "cpl";
469                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
470         }
471
472         string const pkl_uuid = make_uuid ();
473         NameFormat::Map values;
474         values['t'] = "pkl";
475         boost::filesystem::path const pkl_path = write_pkl (name_format.get(values, "_" + pkl_uuid + ".xml"), standard, pkl_uuid, metadata, signer);
476
477         write_volindex (standard);
478         write_assetmap (standard, pkl_uuid, 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 ReelAsset> j, i->reel_assets ()) {
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 }