Add round-trip KDM test. Fix various bugs in KDM generation. Some string -> path.
[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 "signer.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 (boost::filesystem::path 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<Signer> signer) const
71 {
72         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
73                 (*i)->write_xml (interop, metadata, signer);
74         }
75
76         string pkl_uuid = make_uuid ();
77         string pkl_path = write_pkl (pkl_uuid, interop, metadata, signer);
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<Signer> signer) 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 (signer) {
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 (signer) {
124                 signer->sign (pkl, 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 void
198 DCP::read (bool require_mxfs)
199 {
200         read_assets ();
201         read_cpls (require_mxfs);
202 }
203
204 void
205 DCP::read_assets ()
206 {
207         shared_ptr<parse::AssetMap> asset_map;
208         try {
209                 boost::filesystem::path p = _directory;
210                 p /= "ASSETMAP";
211                 if (boost::filesystem::exists (p)) {
212                         asset_map.reset (new libdcp::parse::AssetMap (p.string ()));
213                 } else {
214                         p = _directory;
215                         p /= "ASSETMAP.xml";
216                         if (boost::filesystem::exists (p)) {
217                                 asset_map.reset (new libdcp::parse::AssetMap (p.string ()));
218                         } else {
219                                 boost::throw_exception (DCPReadError ("could not find AssetMap file"));
220                         }
221                 }
222                 
223         } catch (FileError& e) {
224                 boost::throw_exception (FileError ("could not load AssetMap file", _files.asset_map));
225         }
226
227         for (list<shared_ptr<libdcp::parse::AssetMapAsset> >::const_iterator i = asset_map->assets.begin(); i != asset_map->assets.end(); ++i) {
228                 if ((*i)->chunks.size() != 1) {
229                         boost::throw_exception (XMLError ("unsupported asset chunk count"));
230                 }
231
232                 boost::filesystem::path t = _directory;
233                 t /= (*i)->chunks.front()->path;
234                 
235                 if (boost::algorithm::ends_with (t.string(), ".mxf") || boost::algorithm::ends_with (t.string(), ".ttf")) {
236                         continue;
237                 }
238
239                 xmlpp::DomParser* p = new xmlpp::DomParser;
240                 try {
241                         p->parse_file (t.string());
242                 } catch (std::exception& e) {
243                         delete p;
244                         continue;
245                 }
246
247                 string const root = p->get_document()->get_root_node()->get_name ();
248                 delete p;
249
250                 if (root == "CompositionPlaylist") {
251                         _files.cpls.push_back (t.string());
252                 } else if (root == "PackingList") {
253                         if (_files.pkl.empty ()) {
254                                 _files.pkl = t.string();
255                         } else {
256                                 boost::throw_exception (DCPReadError ("duplicate PKLs found"));
257                         }
258                 }
259         }
260         
261         if (_files.cpls.empty ()) {
262                 boost::throw_exception (FileError ("no CPL files found", ""));
263         }
264
265         if (_files.pkl.empty ()) {
266                 boost::throw_exception (FileError ("no PKL file found", ""));
267         }
268
269         shared_ptr<parse::PKL> pkl;
270         try {
271                 pkl.reset (new parse::PKL (_files.pkl));
272         } catch (FileError& e) {
273                 boost::throw_exception (FileError ("could not load PKL file", _files.pkl));
274         }
275
276         _asset_maps.push_back (make_pair (boost::filesystem::absolute (_directory).string(), asset_map));
277 }
278
279 void
280 DCP::read_cpls (bool require_mxfs)
281 {
282         for (list<string>::iterator i = _files.cpls.begin(); i != _files.cpls.end(); ++i) {
283                 _cpls.push_back (shared_ptr<CPL> (new CPL (_directory, *i, _asset_maps, require_mxfs)));
284         }
285 }
286
287 bool
288 DCP::equals (DCP const & other, EqualityOptions opt, boost::function<void (NoteType, string)> note) const
289 {
290         if (_cpls.size() != other._cpls.size()) {
291                 note (ERROR, "CPL counts differ");
292                 return false;
293         }
294
295         list<shared_ptr<CPL> >::const_iterator a = _cpls.begin ();
296         list<shared_ptr<CPL> >::const_iterator b = other._cpls.begin ();
297
298         while (a != _cpls.end ()) {
299                 if (!(*a)->equals (*b->get(), opt, note)) {
300                         return false;
301                 }
302                 ++a;
303                 ++b;
304         }
305
306         return true;
307 }
308
309 void
310 DCP::add_cpl (shared_ptr<CPL> cpl)
311 {
312         _cpls.push_back (cpl);
313 }
314
315 class AssetComparator
316 {
317 public:
318         bool operator() (shared_ptr<const Asset> a, shared_ptr<const Asset> b) {
319                 return a->uuid() < b->uuid();
320         }
321 };
322
323 list<shared_ptr<const Asset> >
324 DCP::assets () const
325 {
326         list<shared_ptr<const Asset> > a;
327         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
328                 list<shared_ptr<const Asset> > t = (*i)->assets ();
329                 a.merge (t);
330         }
331
332         a.sort (AssetComparator ());
333         a.unique ();
334         return a;
335 }
336
337 bool
338 DCP::encrypted () const
339 {
340         for (list<shared_ptr<CPL> >::const_iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
341                 if ((*i)->encrypted ()) {
342                         return true;
343                 }
344         }
345
346         return false;
347 }
348
349 void
350 DCP::add_kdm (KDM const & kdm)
351 {
352         list<KDMKey> keys = kdm.keys ();
353         
354         for (list<shared_ptr<CPL> >::iterator i = _cpls.begin(); i != _cpls.end(); ++i) {
355                 for (list<KDMKey>::iterator j = keys.begin(); j != keys.end(); ++j) {
356                         if (j->cpl_id() == (*i)->id()) {
357                                 (*i)->add_kdm (kdm);
358                         }                               
359                 }
360         }
361 }
362
363 void
364 DCP::add_assets_from (DCP const & ov)
365 {
366         copy (ov._asset_maps.begin(), ov._asset_maps.end(), back_inserter (_asset_maps));
367 }