Fix paths in OV DCP searches.
[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 <boost/lexical_cast.hpp>
31 #include <boost/algorithm/string.hpp>
32 #include <boost/lexical_cast.hpp>
33 #include <libxml++/libxml++.h>
34 #include <xmlsec/xmldsig.h>
35 #include <xmlsec/app.h>
36 #include "dcp.h"
37 #include "asset.h"
38 #include "sound_asset.h"
39 #include "picture_asset.h"
40 #include "subtitle_asset.h"
41 #include "util.h"
42 #include "metadata.h"
43 #include "exceptions.h"
44 #include "parse/pkl.h"
45 #include "parse/asset_map.h"
46 #include "reel.h"
47 #include "cpl.h"
48 #include "encryption.h"
49 #include "kdm.h"
50
51 using std::string;
52 using std::list;
53 using std::stringstream;
54 using std::ofstream;
55 using std::ostream;
56 using std::copy;
57 using std::back_inserter;
58 using std::make_pair;
59 using boost::shared_ptr;
60 using boost::lexical_cast;
61 using namespace libdcp;
62
63 DCP::DCP (string directory)
64         : _directory (directory)
65 {
66         boost::filesystem::create_directories (directory);
67 }
68
69 void
70 DCP::write_xml (bool interop, XMLMetadata const & metadata, shared_ptr<Encryption> crypt) const
71 {
72         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
73                 (*i)->write_xml (interop, metadata, crypt);
74         }
75
76         string pkl_uuid = make_uuid ();
77         string pkl_path = write_pkl (pkl_uuid, interop, metadata, crypt);
78         
79         write_volindex ();
80         write_assetmap (pkl_uuid, boost::filesystem::file_size (pkl_path), interop, metadata);
81 }
82
83 std::string
84 DCP::write_pkl (string pkl_uuid, bool interop, XMLMetadata const & metadata, shared_ptr<Encryption> crypt) const
85 {
86         assert (!_cpls.empty ());
87         
88         boost::filesystem::path p;
89         p /= _directory;
90         stringstream s;
91         s << pkl_uuid << "_pkl.xml";
92         p /= s.str();
93
94         xmlpp::Document doc;
95         xmlpp::Element* pkl;
96         if (interop) {
97                 pkl = doc.create_root_node("PackingList", "http://www.digicine.com/PROTO-ASDCP-PKL-20040311#");
98         } else {
99                 pkl = doc.create_root_node("PackingList", "http://www.smpte-ra.org/schemas/429-8/2007/PKL");
100         }
101         
102         if (crypt) {
103                 pkl->set_namespace_declaration ("http://www.w3.org/2000/09/xmldsig#", "dsig");
104         }
105
106         pkl->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
107         /* XXX: this is a bit of a hack */
108         pkl->add_child("AnnotationText")->add_child_text(_cpls.front()->name());
109         pkl->add_child("IssueDate")->add_child_text (metadata.issue_date);
110         pkl->add_child("Issuer")->add_child_text (metadata.issuer);
111         pkl->add_child("Creator")->add_child_text (metadata.creator);
112
113         xmlpp::Element* asset_list = pkl->add_child("AssetList");
114         list<shared_ptr<const Asset> > a = assets ();
115         for (list<shared_ptr<const Asset> >::const_iterator i = a.begin(); i != a.end(); ++i) {
116                 (*i)->write_to_pkl (asset_list);
117         }
118         
119         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
120                 (*i)->write_to_pkl (asset_list);
121         }
122
123         if (crypt) {
124                 sign (pkl, crypt->certificates, crypt->signer_key, interop);
125         }
126                 
127         doc.write_to_file_formatted (p.string (), "UTF-8");
128         return p.string ();
129 }
130
131 void
132 DCP::write_volindex () const
133 {
134         boost::filesystem::path p;
135         p /= _directory;
136         p /= "VOLINDEX.xml";
137
138         xmlpp::Document doc;
139         xmlpp::Element* root = doc.create_root_node ("VolumeIndex", "http://www.smpte-ra.org/schemas/429-9/2007/AM");
140         root->add_child("Index")->add_child_text ("1");
141         doc.write_to_file_formatted (p.string (), "UTF-8");
142 }
143
144 void
145 DCP::write_assetmap (string pkl_uuid, int pkl_length, bool interop, XMLMetadata const & metadata) const
146 {
147         boost::filesystem::path p;
148         p /= _directory;
149         p /= "ASSETMAP.xml";
150
151         xmlpp::Document doc;
152         xmlpp::Element* root;
153         if (interop) {
154                 root = doc.create_root_node ("AssetMap", "http://www.digicine.com/PROTO-ASDCP-AM-20040311#");
155         } else {
156                 root = doc.create_root_node ("AssetMap", "http://www.smpte-ra.org/schemas/429-9/2007/AM");
157         }
158
159         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
160         root->add_child("AnnotationText")->add_child_text ("Created by " + metadata.creator);
161         if (interop) {
162                 root->add_child("VolumeCount")->add_child_text ("1");
163                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
164                 root->add_child("Issuer")->add_child_text (metadata.issuer);
165                 root->add_child("Creator")->add_child_text (metadata.creator);
166         } else {
167                 root->add_child("Creator")->add_child_text (metadata.creator);
168                 root->add_child("VolumeCount")->add_child_text ("1");
169                 root->add_child("IssueDate")->add_child_text (metadata.issue_date);
170                 root->add_child("Issuer")->add_child_text (metadata.issuer);
171         }
172                 
173         xmlpp::Node* asset_list = root->add_child ("AssetList");
174
175         xmlpp::Node* asset = asset_list->add_child ("Asset");
176         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
177         asset->add_child("PackingList")->add_child_text ("true");
178         xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
179         xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
180         chunk->add_child("Path")->add_child_text (pkl_uuid + "_pkl.xml");
181         chunk->add_child("VolumeIndex")->add_child_text ("1");
182         chunk->add_child("Offset")->add_child_text ("0");
183         chunk->add_child("Length")->add_child_text (lexical_cast<string> (pkl_length));
184         
185         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
186                 (*i)->write_to_assetmap (asset_list);
187         }
188
189         list<shared_ptr<const Asset> > a = assets ();
190         for (list<shared_ptr<const Asset> >::const_iterator i = a.begin(); i != a.end(); ++i) {
191                 (*i)->write_to_assetmap (asset_list);
192         }
193
194         doc.write_to_file_formatted (p.string (), "UTF-8");
195 }
196
197
198 void
199 DCP::read (bool require_mxfs)
200 {
201         Files files;
202
203         shared_ptr<parse::AssetMap> asset_map;
204         try {
205                 boost::filesystem::path p = _directory;
206                 p /= "ASSETMAP";
207                 if (boost::filesystem::exists (p)) {
208                         asset_map.reset (new libdcp::parse::AssetMap (p.string ()));
209                 } else {
210                         p = _directory;
211                         p /= "ASSETMAP.xml";
212                         if (boost::filesystem::exists (p)) {
213                                 asset_map.reset (new libdcp::parse::AssetMap (p.string ()));
214                         } else {
215                                 boost::throw_exception (DCPReadError ("could not find AssetMap file"));
216                         }
217                 }
218                 
219         } catch (FileError& e) {
220                 boost::throw_exception (FileError ("could not load AssetMap file", files.asset_map));
221         }
222
223         for (list<shared_ptr<libdcp::parse::AssetMapAsset> >::const_iterator i = asset_map->assets.begin(); i != asset_map->assets.end(); ++i) {
224                 if ((*i)->chunks.size() != 1) {
225                         boost::throw_exception (XMLError ("unsupported asset chunk count"));
226                 }
227
228                 boost::filesystem::path t = _directory;
229                 t /= (*i)->chunks.front()->path;
230                 
231                 if (boost::algorithm::ends_with (t.string(), ".mxf") || boost::algorithm::ends_with (t.string(), ".ttf")) {
232                         continue;
233                 }
234
235                 xmlpp::DomParser* p = new xmlpp::DomParser;
236                 try {
237                         p->parse_file (t.string());
238                 } catch (std::exception& e) {
239                         delete p;
240                         continue;
241                 }
242
243                 string const root = p->get_document()->get_root_node()->get_name ();
244                 delete p;
245
246                 if (root == "CompositionPlaylist") {
247                         files.cpls.push_back (t.string());
248                 } else if (root == "PackingList") {
249                         if (files.pkl.empty ()) {
250                                 files.pkl = t.string();
251                         } else {
252                                 boost::throw_exception (DCPReadError ("duplicate PKLs found"));
253                         }
254                 }
255         }
256         
257         if (files.cpls.empty ()) {
258                 boost::throw_exception (FileError ("no CPL files found", ""));
259         }
260
261         if (files.pkl.empty ()) {
262                 boost::throw_exception (FileError ("no PKL file found", ""));
263         }
264
265         shared_ptr<parse::PKL> pkl;
266         try {
267                 pkl.reset (new parse::PKL (files.pkl));
268         } catch (FileError& e) {
269                 boost::throw_exception (FileError ("could not load PKL file", files.pkl));
270         }
271
272         /* Cross-check */
273         /* XXX */
274
275         _asset_maps.push_back (make_pair (boost::filesystem::absolute (_directory).string(), asset_map));
276
277         for (list<string>::iterator i = files.cpls.begin(); i != files.cpls.end(); ++i) {
278                 _cpls.push_back (shared_ptr<CPL> (new CPL (_directory, *i, _asset_maps, require_mxfs)));
279         }
280 }
281
282 bool
283 DCP::equals (DCP const & other, EqualityOptions opt, boost::function<void (NoteType, string)> note) const
284 {
285         if (_cpls.size() != other._cpls.size()) {
286                 note (ERROR, "CPL counts differ");
287                 return false;
288         }
289
290         list<shared_ptr<CPL> >::const_iterator a = _cpls.begin ();
291         list<shared_ptr<CPL> >::const_iterator b = other._cpls.begin ();
292
293         while (a != _cpls.end ()) {
294                 if (!(*a)->equals (*b->get(), opt, note)) {
295                         return false;
296                 }
297                 ++a;
298                 ++b;
299         }
300
301         return true;
302 }
303
304 void
305 DCP::add_cpl (shared_ptr<CPL> cpl)
306 {
307         _cpls.push_back (cpl);
308 }
309
310 class AssetComparator
311 {
312 public:
313         bool operator() (shared_ptr<const Asset> a, shared_ptr<const Asset> b) {
314                 return a->uuid() < b->uuid();
315         }
316 };
317
318 list<shared_ptr<const Asset> >
319 DCP::assets () const
320 {
321         list<shared_ptr<const Asset> > a;
322         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
323                 list<shared_ptr<const Asset> > t = (*i)->assets ();
324                 a.merge (t);
325         }
326
327         a.sort (AssetComparator ());
328         a.unique ();
329         return a;
330 }
331
332 bool
333 DCP::encrypted () const
334 {
335         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
336                 if ((*i)->encrypted ()) {
337                         return true;
338                 }
339         }
340
341         return false;
342 }
343
344 void
345 DCP::add_kdm (KDM const & kdm)
346 {
347         list<KDMCipher> ciphers = kdm.ciphers ();
348         
349         for (list<shared_ptr<CPL> >::iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
350                 for (list<KDMCipher>::iterator j = ciphers.begin(); j != ciphers.end(); ++j) {
351                         if (j->cpl_id() == (*i)->id()) {
352                                 (*i)->add_kdm (kdm);
353                         }                               
354                 }
355         }
356 }
357
358 void
359 DCP::add_assets_from (DCP const & ov)
360 {
361         copy (ov._asset_maps.begin(), ov._asset_maps.end(), back_inserter (_asset_maps));
362 }