Bv2.1 7.2.3: Check that subtitle <StartTime> exists and is 0.
[libdcp.git] / src / dcp.cc
1 /*
2     Copyright (C) 2012-2021 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 "atmos_asset.h"
42 #include "picture_asset.h"
43 #include "interop_subtitle_asset.h"
44 #include "smpte_subtitle_asset.h"
45 #include "mono_picture_asset.h"
46 #include "stereo_picture_asset.h"
47 #include "reel_subtitle_asset.h"
48 #include "util.h"
49 #include "metadata.h"
50 #include "exceptions.h"
51 #include "cpl.h"
52 #include "certificate_chain.h"
53 #include "compose.hpp"
54 #include "decrypted_kdm.h"
55 #include "decrypted_kdm_key.h"
56 #include "dcp_assert.h"
57 #include "reel_asset.h"
58 #include "font_asset.h"
59 #include "pkl.h"
60 #include "asset_factory.h"
61 #include "verify.h"
62 #include <asdcp/AS_DCP.h>
63 #include <xmlsec/xmldsig.h>
64 #include <xmlsec/app.h>
65 #include <libxml++/libxml++.h>
66 #include <boost/filesystem.hpp>
67 #include <boost/algorithm/string.hpp>
68
69 using std::string;
70 using std::list;
71 using std::vector;
72 using std::cout;
73 using std::make_pair;
74 using std::map;
75 using std::cerr;
76 using std::make_shared;
77 using std::exception;
78 using std::shared_ptr;
79 using std::dynamic_pointer_cast;
80 using boost::optional;
81 using boost::algorithm::starts_with;
82 using namespace dcp;
83
84 static string const assetmap_interop_ns = "http://www.digicine.com/PROTO-ASDCP-AM-20040311#";
85 static string const assetmap_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
86 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
87 static string const volindex_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
88
89 DCP::DCP (boost::filesystem::path directory)
90         : _directory (directory)
91 {
92         if (!boost::filesystem::exists (directory)) {
93                 boost::filesystem::create_directories (directory);
94         }
95
96         _directory = boost::filesystem::canonical (_directory);
97 }
98
99 /** Read a DCP.  This method does not do any deep checking of the DCP's validity, but
100  *  if it comes across any bad things it will do one of two things.
101  *
102  *  Errors that are so serious that they prevent the method from working will result
103  *  in an exception being thrown.  For example, a missing ASSETMAP means that the DCP
104  *  can't be read without a lot of guesswork, so this will throw.
105  *
106  *  Errors that are not fatal will be added to notes, if it's non-0.  For example,
107  *  if the DCP contains a mixture of Interop and SMPTE elements this will result
108  *  in a note being added to the list.
109  */
110 void
111 DCP::read (list<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
112 {
113         /* Read the ASSETMAP and PKL */
114
115         if (boost::filesystem::exists (_directory / "ASSETMAP")) {
116                 _asset_map = _directory / "ASSETMAP";
117         } else if (boost::filesystem::exists (_directory / "ASSETMAP.xml")) {
118                 _asset_map = _directory / "ASSETMAP.xml";
119         } else {
120                 boost::throw_exception (ReadError(String::compose("Could not find ASSETMAP nor ASSETMAP.xml in '%1'", _directory.string())));
121         }
122
123         cxml::Document asset_map ("AssetMap");
124
125         asset_map.read_file (_asset_map.get());
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         auto asset_nodes = asset_map.node_child("AssetList")->node_children ("Asset");
135         map<string, boost::filesystem::path> paths;
136         list<boost::filesystem::path> pkl_paths;
137         for (auto i: asset_nodes) {
138                 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
139                         boost::throw_exception (XMLError ("unsupported asset chunk count"));
140                 }
141                 auto p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
142                 if (starts_with (p, "file://")) {
143                         p = p.substr (7);
144                 }
145                 switch (*_standard) {
146                 case INTEROP:
147                         if (i->optional_node_child("PackingList")) {
148                                 pkl_paths.push_back (p);
149                         } else {
150                                 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
151                         }
152                         break;
153                 case SMPTE:
154                 {
155                         auto pkl_bool = i->optional_string_child("PackingList");
156                         if (pkl_bool && *pkl_bool == "true") {
157                                 pkl_paths.push_back (p);
158                         } else {
159                                 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
160                         }
161                         break;
162                 }
163                 }
164         }
165
166         if (pkl_paths.empty()) {
167                 boost::throw_exception (XMLError ("No packing lists found in asset map"));
168         }
169
170         for (auto i: pkl_paths) {
171                 _pkls.push_back (make_shared<PKL>(_directory / i));
172         }
173
174         /* Now we have:
175              paths - map of files in the DCP that are not PKLs; key is ID, value is path.
176              _pkls - PKL objects for each PKL.
177
178            Read all the assets from the asset map.
179          */
180
181         /* Make a list of non-CPL/PKL assets so that we can resolve the references
182            from the CPLs.
183         */
184         list<shared_ptr<Asset>> other_assets;
185
186         for (auto i: paths) {
187                 auto path = _directory / i.second;
188
189                 if (i.second.empty()) {
190                         /* I can't see how this is valid, but it's
191                            been seen in the wild with a DCP that
192                            claims to come from ClipsterDCI 5.10.0.5.
193                         */
194                         if (notes) {
195                                 notes->push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::EMPTY_ASSET_PATH));
196                         }
197                         continue;
198                 }
199
200                 if (!boost::filesystem::exists(path)) {
201                         if (notes) {
202                                 notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISSING_ASSET, path));
203                         }
204                         continue;
205                 }
206
207                 /* Find the <Type> for this asset from the PKL that contains the asset */
208                 optional<string> pkl_type;
209                 for (auto j: _pkls) {
210                         pkl_type = j->type(i.first);
211                         if (pkl_type) {
212                                 break;
213                         }
214                 }
215
216                 if (!pkl_type) {
217                         /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
218                          * need to worry about it.
219                          */
220                         continue;
221                 }
222
223                 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
224                         auto p = new xmlpp::DomParser;
225                         try {
226                                 p->parse_file (path.string());
227                         } catch (std::exception& e) {
228                                 delete p;
229                                 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
230                         }
231
232                         auto const root = p->get_document()->get_root_node()->get_name ();
233                         delete p;
234
235                         if (root == "CompositionPlaylist") {
236                                 auto cpl = make_shared<CPL>(path);
237                                 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get() && notes) {
238                                         notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
239                                 }
240                                 _cpls.push_back (cpl);
241                         } else if (root == "DCSubtitle") {
242                                 if (_standard && _standard.get() == SMPTE && notes) {
243                                         notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
244                                 }
245                                 other_assets.push_back (make_shared<InteropSubtitleAsset>(path));
246                         }
247                 } else if (
248                         *pkl_type == PictureAsset::static_pkl_type(*_standard) ||
249                         *pkl_type == SoundAsset::static_pkl_type(*_standard) ||
250                         *pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
251                         *pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
252                         ) {
253
254                         other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type));
255                 } else if (*pkl_type == FontAsset::static_pkl_type(*_standard)) {
256                         other_assets.push_back (make_shared<FontAsset>(i.first, path));
257                 } else if (*pkl_type == "image/png") {
258                         /* It's an Interop PNG subtitle; let it go */
259                 } else {
260                         throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
261                 }
262         }
263
264         resolve_refs (other_assets);
265
266         /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
267         if (notes) {
268                 for (auto i: cpls()) {
269                         for (auto j: i->reel_mxfs()) {
270                                 if (!j->asset_ref().resolved() && paths.find(j->asset_ref().id()) == paths.end()) {
271                                         notes->push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::EXTERNAL_ASSET, j->asset_ref().id()));
272                                 }
273                         }
274                 }
275         }
276 }
277
278 void
279 DCP::resolve_refs (list<shared_ptr<Asset>> assets)
280 {
281         for (auto i: cpls()) {
282                 i->resolve_refs (assets);
283         }
284 }
285
286 bool
287 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
288 {
289         auto a = cpls ();
290         auto b = other.cpls ();
291
292         if (a.size() != b.size()) {
293                 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
294                 return false;
295         }
296
297         bool r = true;
298
299         for (auto i: a) {
300                 auto j = b.begin();
301                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
302                         ++j;
303                 }
304
305                 if (j == b.end ()) {
306                         r = false;
307                 }
308         }
309
310         return r;
311 }
312
313 void
314 DCP::add (std::shared_ptr<CPL> cpl)
315 {
316         _cpls.push_back (cpl);
317 }
318
319 bool
320 DCP::encrypted () const
321 {
322         for (auto i: cpls()) {
323                 if (i->encrypted()) {
324                         return true;
325                 }
326         }
327
328         return false;
329 }
330
331 /** Add a KDM to decrypt this DCP.  This method must be called after DCP::read()
332  *  or the KDM you specify will be ignored.
333  *  @param kdm KDM to use.
334  */
335 void
336 DCP::add (DecryptedKDM const & kdm)
337 {
338         auto keys = kdm.keys ();
339
340         for (auto i: cpls()) {
341                 for (auto const& j: kdm.keys()) {
342                         if (j.cpl_id() == i->id()) {
343                                 i->add (kdm);
344                         }
345                 }
346         }
347 }
348
349 /** Write the VOLINDEX file.
350  *  @param standard DCP standard to use (INTEROP or SMPTE)
351  */
352 void
353 DCP::write_volindex (Standard standard) const
354 {
355         boost::filesystem::path p = _directory;
356         switch (standard) {
357         case INTEROP:
358                 p /= "VOLINDEX";
359                 break;
360         case SMPTE:
361                 p /= "VOLINDEX.xml";
362                 break;
363         default:
364                 DCP_ASSERT (false);
365         }
366
367         xmlpp::Document doc;
368         xmlpp::Element* root;
369
370         switch (standard) {
371         case INTEROP:
372                 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
373                 break;
374         case SMPTE:
375                 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
376                 break;
377         default:
378                 DCP_ASSERT (false);
379         }
380
381         root->add_child("Index")->add_child_text ("1");
382         doc.write_to_file_formatted (p.string (), "UTF-8");
383 }
384
385 void
386 DCP::write_assetmap (
387         Standard standard, string pkl_uuid, boost::filesystem::path pkl_path,
388         string issuer, string creator, string issue_date, string annotation_text
389         ) const
390 {
391         auto p = _directory;
392
393         switch (standard) {
394         case INTEROP:
395                 p /= "ASSETMAP";
396                 break;
397         case SMPTE:
398                 p /= "ASSETMAP.xml";
399                 break;
400         default:
401                 DCP_ASSERT (false);
402         }
403
404         xmlpp::Document doc;
405         xmlpp::Element* root;
406
407         switch (standard) {
408         case INTEROP:
409                 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
410                 break;
411         case SMPTE:
412                 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
413                 break;
414         default:
415                 DCP_ASSERT (false);
416         }
417
418         root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
419         root->add_child("AnnotationText")->add_child_text (annotation_text);
420
421         switch (standard) {
422         case INTEROP:
423                 root->add_child("VolumeCount")->add_child_text ("1");
424                 root->add_child("IssueDate")->add_child_text (issue_date);
425                 root->add_child("Issuer")->add_child_text (issuer);
426                 root->add_child("Creator")->add_child_text (creator);
427                 break;
428         case SMPTE:
429                 root->add_child("Creator")->add_child_text (creator);
430                 root->add_child("VolumeCount")->add_child_text ("1");
431                 root->add_child("IssueDate")->add_child_text (issue_date);
432                 root->add_child("Issuer")->add_child_text (issuer);
433                 break;
434         default:
435                 DCP_ASSERT (false);
436         }
437
438         auto asset_list = root->add_child ("AssetList");
439
440         auto asset = asset_list->add_child ("Asset");
441         asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
442         asset->add_child("PackingList")->add_child_text ("true");
443         auto chunk_list = asset->add_child ("ChunkList");
444         auto chunk = chunk_list->add_child ("Chunk");
445         chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
446         chunk->add_child("VolumeIndex")->add_child_text ("1");
447         chunk->add_child("Offset")->add_child_text ("0");
448         chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
449
450         for (auto i: assets()) {
451                 i->write_to_assetmap (asset_list, _directory);
452         }
453
454         doc.write_to_file_formatted (p.string (), "UTF-8");
455         _asset_map = p;
456 }
457
458 /** Write all the XML files for this DCP.
459  *  @param standand INTEROP or SMPTE.
460  *  @param metadata Metadata to use for PKL and asset map files.
461  *  @param signer Signer to use, or 0.
462  */
463 void
464 DCP::write_xml (
465         Standard standard,
466         string issuer,
467         string creator,
468         string issue_date,
469         string annotation_text,
470         shared_ptr<const CertificateChain> signer,
471         NameFormat name_format
472         )
473 {
474         for (auto i: cpls()) {
475                 NameFormat::Map values;
476                 values['t'] = "cpl";
477                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
478         }
479
480         shared_ptr<PKL> pkl;
481
482         if (_pkls.empty()) {
483                 pkl = make_shared<PKL>(standard, annotation_text, issue_date, issuer, creator);
484                 _pkls.push_back (pkl);
485                 for (auto i: assets()) {
486                         i->add_to_pkl (pkl, _directory);
487                 }
488         } else {
489                 pkl = _pkls.front ();
490         }
491
492         NameFormat::Map values;
493         values['t'] = "pkl";
494         auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
495         pkl->write (pkl_path, signer);
496
497         write_volindex (standard);
498         write_assetmap (standard, pkl->id(), pkl_path, issuer, creator, issue_date, annotation_text);
499 }
500
501 list<shared_ptr<CPL>>
502 DCP::cpls () const
503 {
504         return _cpls;
505 }
506
507 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
508  *  an exception is thrown if they are found.
509  *  @return All assets (including CPLs).
510  */
511 list<shared_ptr<Asset>>
512 DCP::assets (bool ignore_unresolved) const
513 {
514         list<shared_ptr<Asset>> assets;
515         for (auto i: cpls()) {
516                 assets.push_back (i);
517                 for (auto j: i->reel_mxfs()) {
518                         if (ignore_unresolved && !j->asset_ref().resolved()) {
519                                 continue;
520                         }
521
522                         auto const id = j->asset_ref().id();
523                         auto already_got = false;
524                         for (auto k: assets) {
525                                 if (k->id() == id) {
526                                         already_got = true;
527                                 }
528                         }
529
530                         if (!already_got) {
531                                 auto o = j->asset_ref().asset();
532                                 assets.push_back (o);
533                                 /* More Interop special-casing */
534                                 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
535                                 if (sub) {
536                                         sub->add_font_assets (assets);
537                                 }
538                         }
539                 }
540         }
541
542         return assets;
543 }
544
545 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
546 vector<boost::filesystem::path>
547 DCP::directories_from_files (vector<boost::filesystem::path> files)
548 {
549         vector<boost::filesystem::path> d;
550         for (auto i: files) {
551                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
552                         d.push_back (i.parent_path ());
553                 }
554         }
555         return d;
556 }