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