2a1a156b3fcf5ba7f2c36fb71a172cf73f792de2
[libdcp.git] / src / dcp.cc
1 /*
2     Copyright (C) 2012 Carl Hetherington <cth@carlh.net>
3
4     This program is free software; you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation; either version 2 of the License, or
7     (at your option) any later version.
8
9     This program is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with this program; if not, write to the Free Software
16     Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
17
18 */
19
20 /** @file  src/dcp.cc
21  *  @brief A class to create a DCP.
22  */
23
24 #include <sstream>
25 #include <fstream>
26 #include <iomanip>
27 #include <cassert>
28 #include <iostream>
29 #include <boost/filesystem.hpp>
30 #include <libxml++/libxml++.h>
31 #include <xmlsec/xmldsig.h>
32 #include <xmlsec/app.h>
33 #include "dcp.h"
34 #include "asset.h"
35 #include "sound_asset.h"
36 #include "picture_asset.h"
37 #include "subtitle_asset.h"
38 #include "util.h"
39 #include "metadata.h"
40 #include "exceptions.h"
41 #include "cpl_file.h"
42 #include "pkl_file.h"
43 #include "asset_map.h"
44 #include "reel.h"
45
46 using std::string;
47 using std::list;
48 using std::stringstream;
49 using std::ofstream;
50 using std::ostream;
51 using boost::shared_ptr;
52 using namespace libdcp;
53
54 DCP::DCP (string directory)
55         : _directory (directory)
56         , _encrypted (false)
57 {
58         boost::filesystem::create_directories (directory);
59 }
60
61 void
62 DCP::write_xml () const
63 {
64         for (list<shared_ptr<const CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
65                 (*i)->write_xml (_encrypted, _certificates, _signer_key);
66         }
67
68         string pkl_uuid = make_uuid ();
69         string pkl_path = write_pkl (pkl_uuid);
70         
71         write_volindex ();
72         write_assetmap (pkl_uuid, boost::filesystem::file_size (pkl_path));
73 }
74
75 std::string
76 DCP::write_pkl (string pkl_uuid) const
77 {
78         assert (!_cpls.empty ());
79         
80         boost::filesystem::path p;
81         p /= _directory;
82         stringstream s;
83         s << pkl_uuid << "_pkl.xml";
84         p /= s.str();
85         ofstream pkl (p.string().c_str());
86
87         pkl << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
88             << "<PackingList xmlns=\"http://www.smpte-ra.org/schemas/429-8/2007/PKL\">\n"
89             << "  <Id>urn:uuid:" << pkl_uuid << "</Id>\n"
90                 /* XXX: this is a bit of a hack */
91             << "  <AnnotationText>" << _cpls.front()->name() << "</AnnotationText>\n"
92             << "  <IssueDate>" << Metadata::instance()->issue_date << "</IssueDate>\n"
93             << "  <Issuer>" << Metadata::instance()->issuer << "</Issuer>\n"
94             << "  <Creator>" << Metadata::instance()->creator << "</Creator>\n"
95             << "  <AssetList>\n";
96
97         list<shared_ptr<const Asset> > a = assets ();
98         for (list<shared_ptr<const Asset> >::const_iterator i = a.begin(); i != a.end(); ++i) {
99                 (*i)->write_to_pkl (pkl);
100         }
101
102         for (list<shared_ptr<const CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
103                 (*i)->write_to_pkl (pkl);
104         }
105
106         pkl << "  </AssetList>\n"
107             << "</PackingList>\n";
108
109         return p.string ();
110 }
111
112 void
113 DCP::write_volindex () const
114 {
115         boost::filesystem::path p;
116         p /= _directory;
117         p /= "VOLINDEX.xml";
118         ofstream vi (p.string().c_str());
119
120         vi << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
121            << "<VolumeIndex xmlns=\"http://www.smpte-ra.org/schemas/429-9/2007/AM\">\n"
122            << "  <Index>1</Index>\n"
123            << "</VolumeIndex>\n";
124 }
125
126 void
127 DCP::write_assetmap (string pkl_uuid, int pkl_length) const
128 {
129         boost::filesystem::path p;
130         p /= _directory;
131         p /= "ASSETMAP.xml";
132         ofstream am (p.string().c_str());
133
134         am << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
135            << "<AssetMap xmlns=\"http://www.smpte-ra.org/schemas/429-9/2007/AM\">\n"
136            << "  <Id>urn:uuid:" << make_uuid() << "</Id>\n"
137            << "  <Creator>" << Metadata::instance()->creator << "</Creator>\n"
138            << "  <VolumeCount>1</VolumeCount>\n"
139            << "  <IssueDate>" << Metadata::instance()->issue_date << "</IssueDate>\n"
140            << "  <Issuer>" << Metadata::instance()->issuer << "</Issuer>\n"
141            << "  <AssetList>\n";
142
143         am << "    <Asset>\n"
144            << "      <Id>urn:uuid:" << pkl_uuid << "</Id>\n"
145            << "      <PackingList>true</PackingList>\n"
146            << "      <ChunkList>\n"
147            << "        <Chunk>\n"
148            << "          <Path>" << pkl_uuid << "_pkl.xml</Path>\n"
149            << "          <VolumeIndex>1</VolumeIndex>\n"
150            << "          <Offset>0</Offset>\n"
151            << "          <Length>" << pkl_length << "</Length>\n"
152            << "        </Chunk>\n"
153            << "      </ChunkList>\n"
154            << "    </Asset>\n";
155         
156         for (list<shared_ptr<const CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
157                 (*i)->write_to_assetmap (am);
158         }
159
160         list<shared_ptr<const Asset> > a = assets ();
161         for (list<shared_ptr<const Asset> >::const_iterator i = a.begin(); i != a.end(); ++i) {
162                 (*i)->write_to_assetmap (am);
163         }
164
165         am << "  </AssetList>\n"
166            << "</AssetMap>\n";
167 }
168
169
170 void
171 DCP::read (bool require_mxfs)
172 {
173         Files files;
174
175         shared_ptr<AssetMap> asset_map;
176         try {
177                 boost::filesystem::path p = _directory;
178                 p /= "ASSETMAP";
179                 if (boost::filesystem::exists (p)) {
180                         asset_map.reset (new AssetMap (p.string ()));
181                 } else {
182                         p = _directory;
183                         p /= "ASSETMAP.xml";
184                         if (boost::filesystem::exists (p)) {
185                                 asset_map.reset (new AssetMap (p.string ()));
186                         } else {
187                                 throw DCPReadError ("could not find AssetMap file");
188                         }
189                 }
190                 
191         } catch (FileError& e) {
192                 throw FileError ("could not load AssetMap file", files.asset_map);
193         }
194
195         for (list<shared_ptr<AssetMapAsset> >::const_iterator i = asset_map->assets.begin(); i != asset_map->assets.end(); ++i) {
196                 if ((*i)->chunks.size() != 1) {
197                         throw XMLError ("unsupported asset chunk count");
198                 }
199
200                 boost::filesystem::path t = _directory;
201                 t /= (*i)->chunks.front()->path;
202                 
203                 if (ends_with (t.string(), ".mxf") || ends_with (t.string(), ".ttf")) {
204                         continue;
205                 }
206
207                 xmlpp::DomParser* p = new xmlpp::DomParser;
208                 try {
209                         p->parse_file (t.string());
210                 } catch (std::exception& e) {
211                         delete p;
212                         continue;
213                 }
214
215                 string const root = p->get_document()->get_root_node()->get_name ();
216                 delete p;
217
218                 if (root == "CompositionPlaylist") {
219                         files.cpls.push_back (t.string());
220                 } else if (root == "PackingList") {
221                         if (files.pkl.empty ()) {
222                                 files.pkl = t.string();
223                         } else {
224                                 throw DCPReadError ("duplicate PKLs found");
225                         }
226                 }
227         }
228         
229         if (files.cpls.empty ()) {
230                 throw FileError ("no CPL files found", "");
231         }
232
233         if (files.pkl.empty ()) {
234                 throw FileError ("no PKL file found", "");
235         }
236
237         shared_ptr<PKLFile> pkl;
238         try {
239                 pkl.reset (new PKLFile (files.pkl));
240         } catch (FileError& e) {
241                 throw FileError ("could not load PKL file", files.pkl);
242         }
243
244         /* Cross-check */
245         /* XXX */
246
247         for (list<string>::iterator i = files.cpls.begin(); i != files.cpls.end(); ++i) {
248                 _cpls.push_back (shared_ptr<CPL> (new CPL (_directory, *i, asset_map, require_mxfs)));
249         }
250 }
251
252 bool
253 DCP::equals (DCP const & other, EqualityOptions opt, list<string>& notes) const
254 {
255         if (_cpls.size() != other._cpls.size()) {
256                 notes.push_back ("CPL counts differ");
257                 return false;
258         }
259
260         list<shared_ptr<const CPL> >::const_iterator a = _cpls.begin ();
261         list<shared_ptr<const CPL> >::const_iterator b = other._cpls.begin ();
262
263         while (a != _cpls.end ()) {
264                 if (!(*a)->equals (*b->get(), opt, notes)) {
265                         return false;
266                 }
267                 ++a;
268                 ++b;
269         }
270
271         return true;
272 }
273
274
275 void
276 DCP::add_cpl (shared_ptr<CPL> cpl)
277 {
278         _cpls.push_back (cpl);
279 }
280
281 class AssetComparator
282 {
283 public:
284         bool operator() (shared_ptr<const Asset> a, shared_ptr<const Asset> b) {
285                 return a->uuid() < b->uuid();
286         }
287 };
288
289 list<shared_ptr<const Asset> >
290 DCP::assets () const
291 {
292         list<shared_ptr<const Asset> > a;
293         for (list<shared_ptr<const CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
294                 list<shared_ptr<const Asset> > t = (*i)->assets ();
295                 a.merge (t);
296         }
297
298         a.sort (AssetComparator ());
299         a.unique ();
300         return a;
301 }
302
303 CPL::CPL (string directory, string name, ContentKind content_kind, int length, int frames_per_second)
304         : _directory (directory)
305         , _name (name)
306         , _content_kind (content_kind)
307         , _length (length)
308         , _fps (frames_per_second)
309 {
310         _uuid = make_uuid ();
311 }
312
313 CPL::CPL (string directory, string file, shared_ptr<const AssetMap> asset_map, bool require_mxfs)
314         : _directory (directory)
315         , _content_kind (FEATURE)
316         , _length (0)
317         , _fps (0)
318 {
319         /* Read the XML */
320         shared_ptr<CPLFile> cpl;
321         try {
322                 cpl.reset (new CPLFile (file));
323         } catch (FileError& e) {
324                 throw FileError ("could not load CPL file", file);
325         }
326         
327         /* Now cherry-pick the required bits into our own data structure */
328         
329         _name = cpl->annotation_text;
330         _content_kind = cpl->content_kind;
331
332         for (list<shared_ptr<CPLReel> >::iterator i = cpl->reels.begin(); i != cpl->reels.end(); ++i) {
333
334                 shared_ptr<Picture> p;
335
336                 if ((*i)->asset_list->main_picture) {
337                         p = (*i)->asset_list->main_picture;
338                 } else {
339                         p = (*i)->asset_list->main_stereoscopic_picture;
340                 }
341                 
342                 _fps = p->edit_rate.numerator;
343                 _length += p->duration;
344
345                 shared_ptr<PictureAsset> picture;
346                 shared_ptr<SoundAsset> sound;
347                 shared_ptr<SubtitleAsset> subtitle;
348
349                 /* Some rather twisted logic to decide if we are 3D or not;
350                    some DCPs give a MainStereoscopicPicture to indicate 3D, others
351                    just have a FrameRate twice the EditRate and apparently
352                    expect you to divine the fact that they are hence 3D.
353                 */
354
355                 if (!(*i)->asset_list->main_stereoscopic_picture && p->edit_rate == p->frame_rate) {
356
357                         try {
358                                 picture.reset (new MonoPictureAsset (
359                                                        _directory,
360                                                        asset_map->asset_from_id (p->id)->chunks.front()->path,
361                                                        _fps,
362                                                        (*i)->asset_list->main_picture->entry_point,
363                                                        (*i)->asset_list->main_picture->duration
364                                                        )
365                                         );
366                         } catch (MXFFileError) {
367                                 if (require_mxfs) {
368                                         throw;
369                                 }
370                         }
371                         
372                 } else {
373
374                         try {
375                                 picture.reset (new StereoPictureAsset (
376                                                        _directory,
377                                                        asset_map->asset_from_id (p->id)->chunks.front()->path,
378                                                        _fps,
379                                                        p->entry_point,
380                                                        p->duration
381                                                        )
382                                         );
383                         } catch (MXFFileError) {
384                                 if (require_mxfs) {
385                                         throw;
386                                 }
387                         }
388                         
389                 }
390                 
391                 if ((*i)->asset_list->main_sound) {
392                         
393                         try {
394                                 sound.reset (new SoundAsset (
395                                                      _directory,
396                                                      asset_map->asset_from_id ((*i)->asset_list->main_sound->id)->chunks.front()->path,
397                                                      _fps,
398                                                      (*i)->asset_list->main_sound->entry_point,
399                                                      (*i)->asset_list->main_sound->duration
400                                                      )
401                                         );
402                         } catch (MXFFileError) {
403                                 if (require_mxfs) {
404                                         throw;
405                                 }
406                         }
407                 }
408
409                 if ((*i)->asset_list->main_subtitle) {
410                         
411                         subtitle.reset (new SubtitleAsset (
412                                                 _directory,
413                                                 asset_map->asset_from_id ((*i)->asset_list->main_subtitle->id)->chunks.front()->path
414                                                 )
415                                 );
416                 }
417                         
418                 _reels.push_back (shared_ptr<Reel> (new Reel (picture, sound, subtitle)));
419         }
420 }
421
422 void
423 CPL::add_reel (shared_ptr<const Reel> reel)
424 {
425         _reels.push_back (reel);
426 }
427
428 void
429 CPL::write_xml (bool encrypted, CertificateChain const & certificates, string const & signer_key) const
430 {
431         boost::filesystem::path p;
432         p /= _directory;
433         stringstream s;
434         s << _uuid << "_cpl.xml";
435         p /= s.str();
436
437         xmlpp::Document doc;
438         xmlpp::Element* cpl = doc.create_root_node("CompositionPlaylist", "http://www.smpte-ra.org/schemas/429-7/2006/CPL");
439
440         if (encrypted) {
441                 cpl->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
442         }
443
444         cpl->add_child("Id")->add_child_text ("urn:uuid:" + _uuid);
445         cpl->add_child("AnnotationText")->add_child_text (_name);
446         cpl->add_child("IssueDate")->add_child_text (Metadata::instance()->issue_date);
447         cpl->add_child("Creator")->add_child_text (Metadata::instance()->creator);
448         cpl->add_child("ContentTitleText")->add_child_text (_name);
449         cpl->add_child("ContentKind")->add_child_text (content_kind_to_string (_content_kind));
450
451         {
452                 xmlpp::Element* cv = cpl->add_child ("ContentVersion");
453                 cv->add_child("Id")->add_child_text ("urn:uri:" + _uuid + "_" + Metadata::instance()->issue_date);
454                 cv->add_child("LabelText")->add_child_text (_uuid + "_" + Metadata::instance()->issue_date);
455         }
456
457         cpl->add_child("RatingList");
458
459         xmlpp::Element* reel_list = cpl->add_child("ReelList");
460         for (list<shared_ptr<const Reel> >::const_iterator i = _reels.begin(); i != _reels.end(); ++i) {
461                 (*i)->write_to_cpl (reel_list);
462         }
463
464         if (encrypted) {
465                 xmlpp::Element* signer = cpl->add_child("Signer");
466                 {
467                         xmlpp::Element* data = signer->add_child("X509Data", "dsig");
468                         {
469                                 xmlpp::Element* serial = data->add_child("X509IssuerSerial", "dsig");
470                                 serial->add_child("X509IssuerName", "dsig")->add_child_text (
471                                         Certificate::name_for_xml (certificates.leaf()->issuer())
472                                         );
473                                 serial->add_child("X509SerialNumber", "dsig")->add_child_text (
474                                         certificates.leaf()->serial()
475                                         );
476                         }
477                         data->add_child("X509SubjectName", "dsig")->add_child_text (
478                                 Certificate::name_for_xml (certificates.leaf()->subject())
479                                 );
480                 }
481
482                 xmlpp::Element* signature = cpl->add_child("Signature", "dsig");
483                 
484                 {
485                         xmlpp::Element* signed_info = signature->add_child ("SignedInfo", "dsig");
486                         signed_info->add_child("CanonicalizationMethod", "dsig")->set_attribute ("Algorithm", "http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
487                         signed_info->add_child("SignatureMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256");
488                         {
489                                 xmlpp::Element* reference = signed_info->add_child("Reference", "dsig");
490                                 reference->set_attribute ("URI", "");
491                                 {
492                                         xmlpp::Element* transforms = reference->add_child("Transforms", "dsig");
493                                         transforms->add_child("Transform", "dsig")->set_attribute (
494                                                 "Algorithm", "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
495                                                 );
496                                 }
497                                 reference->add_child("DigestMethod", "dsig")->set_attribute("Algorithm", "http://www.w3.org/2000/09/xmldsig#sha1");
498                                 /* This will be filled in by the signing later */
499                                 reference->add_child("DigestValue", "dsig");
500                         }
501                 }
502
503                 signature->add_child("SignatureValue", "dsig");
504
505                 xmlpp::Element* key_info = signature->add_child("KeyInfo", "dsig");
506                 list<shared_ptr<Certificate> > c = certificates.leaf_to_root ();
507                 for (list<shared_ptr<Certificate> >::iterator i = c.begin(); i != c.end(); ++i) {
508                         xmlpp::Element* data = key_info->add_child("X509Data", "dsig");
509                         {
510                                 xmlpp::Element* serial = data->add_child("X509IssuerSerial", "dsig");
511                                 serial->add_child("X509IssuerName", "dsig")->add_child_text(
512                                         Certificate::name_for_xml ((*i)->issuer())
513                                         );
514                                 serial->add_child("X509SerialNumber", "dsig")->add_child_text((*i)->serial());
515                         }
516                 }
517
518                 xmlSecKeysMngrPtr keys_manager = xmlSecKeysMngrCreate();
519                 if (!keys_manager) {
520                         throw MiscError ("could not create keys manager");
521                 }
522                 if (xmlSecCryptoAppDefaultKeysMngrInit (keys_manager) < 0) {
523                         throw MiscError ("could not initialise keys manager");
524                 }
525
526                 xmlSecKeyPtr const key = xmlSecCryptoAppKeyLoad (signer_key.c_str(), xmlSecKeyDataFormatPem, 0, 0, 0);
527                 if (key == 0) {
528                         throw MiscError ("could not load signer key");
529                 }
530
531                 if (xmlSecCryptoAppDefaultKeysMngrAdoptKey (keys_manager, key) < 0) {
532                         xmlSecKeyDestroy (key);
533                         throw MiscError ("could not use signer key");
534                 }
535
536                 xmlSecDSigCtx signature_context;
537
538                 if (xmlSecDSigCtxInitialize (&signature_context, keys_manager) < 0) {
539                         throw MiscError ("could not initialise XMLSEC context");
540                 }
541
542                 if (xmlSecDSigCtxSign (&signature_context, signature->cobj()) < 0) {
543                         throw MiscError ("could not sign CPL");
544                 }
545
546                 xmlSecDSigCtxFinalize (&signature_context);
547                 xmlSecKeysMngrDestroy (keys_manager);
548         }
549
550         doc.write_to_file_formatted (p.string(), "UTF-8");
551
552         _digest = make_digest (p.string (), 0);
553         _length = boost::filesystem::file_size (p.string ());
554 }
555
556 void
557 CPL::write_to_pkl (ostream& s) const
558 {
559         s << "    <Asset>\n"
560           << "      <Id>urn:uuid:" << _uuid << "</Id>\n"
561           << "      <Hash>" << _digest << "</Hash>\n"
562           << "      <Size>" << _length << "</Size>\n"
563           << "      <Type>text/xml</Type>\n"
564           << "    </Asset>\n";
565 }
566
567 list<shared_ptr<const Asset> >
568 CPL::assets () const
569 {
570         list<shared_ptr<const Asset> > a;
571         for (list<shared_ptr<const Reel> >::const_iterator i = _reels.begin(); i != _reels.end(); ++i) {
572                 if ((*i)->main_picture ()) {
573                         a.push_back ((*i)->main_picture ());
574                 }
575                 if ((*i)->main_sound ()) {
576                         a.push_back ((*i)->main_sound ());
577                 }
578                 if ((*i)->main_subtitle ()) {
579                         a.push_back ((*i)->main_subtitle ());
580                 }
581         }
582
583         return a;
584 }
585
586 void
587 CPL::write_to_assetmap (ostream& s) const
588 {
589         s << "    <Asset>\n"
590           << "      <Id>urn:uuid:" << _uuid << "</Id>\n"
591           << "      <ChunkList>\n"
592           << "        <Chunk>\n"
593           << "          <Path>" << _uuid << "_cpl.xml</Path>\n"
594           << "          <VolumeIndex>1</VolumeIndex>\n"
595           << "          <Offset>0</Offset>\n"
596           << "          <Length>" << _length << "</Length>\n"
597           << "        </Chunk>\n"
598           << "      </ChunkList>\n"
599           << "    </Asset>\n";
600 }
601         
602         
603         
604 bool
605 CPL::equals (CPL const & other, EqualityOptions opt, list<string>& notes) const
606 {
607         if (_name != other._name) {
608                 notes.push_back ("names differ");
609                 return false;
610         }
611
612         if (_content_kind != other._content_kind) {
613                 notes.push_back ("content kinds differ");
614                 return false;
615         }
616
617         if (_fps != other._fps) {
618                 notes.push_back ("frames per second differ");
619                 return false;
620         }
621
622         if (_length != other._length) {
623                 notes.push_back ("lengths differ");
624                 return false;
625         }
626
627         if (_reels.size() != other._reels.size()) {
628                 notes.push_back ("reel counts differ");
629                 return false;
630         }
631         
632         list<shared_ptr<const Reel> >::const_iterator a = _reels.begin ();
633         list<shared_ptr<const Reel> >::const_iterator b = other._reels.begin ();
634         
635         while (a != _reels.end ()) {
636                 if (!(*a)->equals (*b, opt, notes)) {
637                         return false;
638                 }
639                 ++a;
640                 ++b;
641         }
642
643         return true;
644 }