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