Always use ID in custom filenames.
[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 "picture_asset.h"
42 #include "interop_subtitle_asset.h"
43 #include "smpte_subtitle_asset.h"
44 #include "mono_picture_asset.h"
45 #include "stereo_picture_asset.h"
46 #include "reel_subtitle_asset.h"
47 #include "util.h"
48 #include "metadata.h"
49 #include "exceptions.h"
50 #include "cpl.h"
51 #include "certificate_chain.h"
52 #include "compose.hpp"
53 #include "decrypted_kdm.h"
54 #include "decrypted_kdm_key.h"
55 #include "dcp_assert.h"
56 #include "reel_asset.h"
57 #include "font_asset.h"
58 #include <asdcp/AS_DCP.h>
59 #include <xmlsec/xmldsig.h>
60 #include <xmlsec/app.h>
61 #include <libxml++/libxml++.h>
62 #include <boost/filesystem.hpp>
63 #include <boost/algorithm/string.hpp>
64 #include <boost/foreach.hpp>
65
66 using std::string;
67 using std::list;
68 using std::cout;
69 using std::make_pair;
70 using std::map;
71 using std::cout;
72 using std::cerr;
73 using std::exception;
74 using boost::shared_ptr;
75 using boost::dynamic_pointer_cast;
76 using boost::algorithm::starts_with;
77 using namespace dcp;
78
79 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
80 static string const assetmap_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
81 static string const pkl_interop_ns      = "http://www.digicine.com/PROTO-ASDCP-PKL-20040311#";
82 static string const pkl_smpte_ns        = "http://www.smpte-ra.org/schemas/429-8/2007/PKL";
83 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
84 static string const volindex_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
85
86 DCP::DCP (boost::filesystem::path directory)
87         : _directory (directory)
88 {
89         if (!boost::filesystem::exists (directory)) {
90                 boost::filesystem::create_directories (directory);
91         }
92
93         _directory = boost::filesystem::canonical (_directory);
94 }
95
96 /** Call this instead of throwing an exception if the error can be tolerated */
97 template<class T> void
98 survivable_error (bool keep_going, dcp::DCP::ReadErrors* errors, T const & e)
99 {
100         if (keep_going) {
101                 if (errors) {
102                         errors->push_back (shared_ptr<T> (new T (e)));
103                 }
104         } else {
105                 throw e;
106         }
107 }
108
109 void
110 DCP::read (bool keep_going, ReadErrors* errors, bool ignore_incorrect_picture_mxf_type)
111 {
112         /* Read the ASSETMAP */
113
114         boost::filesystem::path asset_map_file;
115         if (boost::filesystem::exists (_directory / "ASSETMAP")) {
116                 asset_map_file = _directory / "ASSETMAP";
117         } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
118                 asset_map_file = _directory / "ASSETMAP.xml";
119         } else {
120                 boost::throw_exception (DCPReadError (String::compose ("could not find AssetMap file in `%1'", _directory.string())));
121         }
122
123         cxml::Document asset_map ("AssetMap");
124
125         asset_map.read_file (asset_map_file);
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         BOOST_FOREACH (shared_ptr<cxml::Node> i, asset_nodes) {
137                 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
138                         boost::throw_exception (XMLError ("unsupported asset chunk count"));
139                 }
140                 string p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
141                 if (starts_with (p, "file://")) {
142                         p = p.substr (7);
143                 }
144                 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
145         }
146
147         /* Read all the assets from the asset map */
148         /* XXX: I think we should be looking at the PKL here to decide type, not
149            the extension of the file.
150         */
151
152         /* Make a list of non-CPL assets so that we can resolve the references
153            from the CPLs.
154         */
155         list<shared_ptr<Asset> > other_assets;
156
157         for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
158                 boost::filesystem::path path = _directory / i->second;
159
160                 if (!boost::filesystem::exists (path)) {
161                         survivable_error (keep_going, errors, MissingAssetError (path));
162                         continue;
163                 }
164
165                 if (boost::filesystem::extension (path) == ".xml") {
166                         xmlpp::DomParser* p = new xmlpp::DomParser;
167                         try {
168                                 p->parse_file (path.string());
169                         } catch (std::exception& e) {
170                                 delete p;
171                                 continue;
172                         }
173
174                         string const root = p->get_document()->get_root_node()->get_name ();
175                         delete p;
176
177                         if (root == "CompositionPlaylist") {
178                                 shared_ptr<CPL> cpl (new CPL (path));
179                                 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get()) {
180                                         survivable_error (keep_going, errors, MismatchedStandardError ());
181                                 }
182                                 _cpls.push_back (cpl);
183                         } else if (root == "DCSubtitle") {
184                                 if (_standard && _standard.get() == SMPTE) {
185                                         survivable_error (keep_going, errors, MismatchedStandardError ());
186                                 }
187                                 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
188                         }
189                 } else if (boost::filesystem::extension (path) == ".mxf") {
190
191                         /* XXX: asdcplib does not appear to support discovery of read MXFs standard
192                            (Interop / SMPTE)
193                         */
194
195                         ASDCP::EssenceType_t type;
196                         if (ASDCP::EssenceType (path.string().c_str(), type) != ASDCP::RESULT_OK) {
197                                 throw DCPReadError ("Could not find essence type");
198                         }
199                         switch (type) {
200                                 case ASDCP::ESS_UNKNOWN:
201                                 case ASDCP::ESS_MPEG2_VES:
202                                         throw DCPReadError ("MPEG2 video essences are not supported");
203                                 case ASDCP::ESS_JPEG_2000:
204                                         try {
205                                                 other_assets.push_back (shared_ptr<MonoPictureAsset> (new MonoPictureAsset (path)));
206                                         } catch (dcp::MXFFileError& e) {
207                                                 if (ignore_incorrect_picture_mxf_type && e.number() == ASDCP::RESULT_SFORMAT) {
208                                                         /* Tried to load it as mono but the error says it's stereo; try that instead */
209                                                         other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
210                                                 } else {
211                                                         throw;
212                                                 }
213                                         }
214                                         break;
215                                 case ASDCP::ESS_PCM_24b_48k:
216                                 case ASDCP::ESS_PCM_24b_96k:
217                                         other_assets.push_back (shared_ptr<SoundAsset> (new SoundAsset (path)));
218                                         break;
219                                 case ASDCP::ESS_JPEG_2000_S:
220                                         other_assets.push_back (shared_ptr<StereoPictureAsset> (new StereoPictureAsset (path)));
221                                         break;
222                                 case ASDCP::ESS_TIMED_TEXT:
223                                         other_assets.push_back (shared_ptr<SMPTESubtitleAsset> (new SMPTESubtitleAsset (path)));
224                                         break;
225                                 default:
226                                         throw DCPReadError ("Unknown MXF essence type");
227                         }
228                 } else if (boost::filesystem::extension (path) == ".ttf") {
229                         other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
230                 }
231         }
232
233         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
234                 i->resolve_refs (other_assets);
235         }
236 }
237
238 void
239 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
240 {
241         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
242                 i->resolve_refs (assets);
243         }
244 }
245
246 bool
247 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
248 {
249         list<shared_ptr<CPL> > a = cpls ();
250         list<shared_ptr<CPL> > b = other.cpls ();
251
252         if (a.size() != b.size()) {
253                 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
254                 return false;
255         }
256
257         bool r = true;
258
259         BOOST_FOREACH (shared_ptr<CPL> i, a) {
260                 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
261                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
262                         ++j;
263                 }
264
265                 if (j == b.end ()) {
266                         r = false;
267                 }
268         }
269
270         return r;
271 }
272
273 void
274 DCP::add (boost::shared_ptr<CPL> cpl)
275 {
276         _cpls.push_back (cpl);
277 }
278
279 bool
280 DCP::encrypted () const
281 {
282         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
283                 if (i->encrypted ()) {
284                         return true;
285                 }
286         }
287
288         return false;
289 }
290
291 void
292 DCP::add (DecryptedKDM const & kdm)
293 {
294         list<DecryptedKDMKey> keys = kdm.keys ();
295
296         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
297                 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
298                         if (j.cpl_id() == i->id()) {
299                                 i->add (kdm);
300                         }
301                 }
302         }
303 }
304
305 /** @return full pathname of PKL file that was written */
306 boost::filesystem::path
307 DCP::write_pkl (string file, Standard standard, string pkl_uuid, XMLMetadata metadata, shared_ptr<const CertificateChain> signer) const
308 {
309         boost::filesystem::path p = _directory;
310         p /= file;
311
312         xmlpp::Document doc;
313         xmlpp::Element* pkl;
314         if (standard == INTEROP) {
315                 pkl = doc.create_root_node("PackingList", pkl_interop_ns);
316         } else {
317                 pkl = doc.create_root_node("PackingList", pkl_smpte_ns);
318         }
319
320         if (signer) {
321                 pkl->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
322         }
323
324         pkl->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
325
326         /* XXX: this is a bit of a hack */
327         DCP_ASSERT (cpls().size() > 0);
328         pkl->add_child("AnnotationText")->add_child_text (cpls().front()->annotation_text ());
329
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 ("Created by " + metadata.creator);
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 /** @return All assets (including CPLs) */
488 list<shared_ptr<Asset> >
489 DCP::assets () const
490 {
491         list<shared_ptr<Asset> > assets;
492         BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
493                 assets.push_back (i);
494                 BOOST_FOREACH (shared_ptr<const ReelAsset> j, i->reel_assets ()) {
495                         shared_ptr<Asset> o = j->asset_ref().asset ();
496                         assets.push_back (o);
497                         /* More Interop special-casing */
498                         shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
499                         if (sub) {
500                                 sub->add_font_assets (assets);
501                         }
502                 }
503         }
504
505         return assets;
506 }