Update PKL assets before writing it.
[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
35 /** @file  src/dcp.cc
36  *  @brief DCP class
37  */
38
39
40 #include "asset_factory.h"
41 #include "atmos_asset.h"
42 #include "certificate_chain.h"
43 #include "compose.hpp"
44 #include "cpl.h"
45 #include "dcp.h"
46 #include "dcp_assert.h"
47 #include "decrypted_kdm.h"
48 #include "decrypted_kdm_key.h"
49 #include "exceptions.h"
50 #include "font_asset.h"
51 #include "interop_subtitle_asset.h"
52 #include "metadata.h"
53 #include "mono_picture_asset.h"
54 #include "picture_asset.h"
55 #include "pkl.h"
56 #include "raw_convert.h"
57 #include "reel_asset.h"
58 #include "reel_subtitle_asset.h"
59 #include "smpte_subtitle_asset.h"
60 #include "sound_asset.h"
61 #include "stereo_picture_asset.h"
62 #include "util.h"
63 #include "verify.h"
64 #include "warnings.h"
65 LIBDCP_DISABLE_WARNINGS
66 #include <asdcp/AS_DCP.h>
67 LIBDCP_ENABLE_WARNINGS
68 #include <xmlsec/xmldsig.h>
69 #include <xmlsec/app.h>
70 LIBDCP_DISABLE_WARNINGS
71 #include <libxml++/libxml++.h>
72 LIBDCP_ENABLE_WARNINGS
73 #include <boost/algorithm/string.hpp>
74 #include <boost/filesystem.hpp>
75 #include <numeric>
76
77
78 using std::cerr;
79 using std::cout;
80 using std::dynamic_pointer_cast;
81 using std::exception;
82 using std::list;
83 using std::make_pair;
84 using std::make_shared;
85 using std::map;
86 using std::shared_ptr;
87 using std::string;
88 using std::vector;
89 using boost::algorithm::starts_with;
90 using boost::optional;
91 using namespace dcp;
92
93
94 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
95 static string const volindex_smpte_ns   = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
96
97
98 DCP::DCP (boost::filesystem::path directory)
99         : _directory (directory)
100 {
101         if (!boost::filesystem::exists (directory)) {
102                 boost::filesystem::create_directories (directory);
103         }
104
105         _directory = boost::filesystem::canonical (_directory);
106 }
107
108
109 void
110 DCP::read (vector<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
111 {
112         /* Read the ASSETMAP and PKL */
113
114         boost::filesystem::path asset_map_path;
115         if (boost::filesystem::exists(_directory / "ASSETMAP")) {
116                 asset_map_path = _directory / "ASSETMAP";
117         } else if (boost::filesystem::exists(_directory / "ASSETMAP.xml")) {
118                 asset_map_path = _directory / "ASSETMAP.xml";
119         } else {
120                 boost::throw_exception(MissingAssetmapError(_directory));
121         }
122
123         _asset_map = AssetMap(asset_map_path);
124         auto const pkl_paths = _asset_map->pkl_paths();
125         auto const standard = _asset_map->standard();
126
127         if (pkl_paths.empty()) {
128                 boost::throw_exception (XMLError ("No packing lists found in asset map"));
129         }
130
131         for (auto i: pkl_paths) {
132                 _pkls.push_back(make_shared<PKL>(i));
133         }
134
135         /* Now we have:
136              paths - map of files in the DCP that are not PKLs; key is ID, value is path.
137              _pkls - PKL objects for each PKL.
138
139            Read all the assets from the asset map.
140          */
141
142         /* Make a list of non-CPL/PKL assets so that we can resolve the references
143            from the CPLs.
144         */
145         vector<shared_ptr<Asset>> other_assets;
146
147         auto ids_and_paths = _asset_map->asset_ids_and_paths();
148         for (auto i: ids_and_paths) {
149                 auto path = i.second;
150
151                 if (path == _directory) {
152                         /* I can't see how this is valid, but it's
153                            been seen in the wild with a DCP that
154                            claims to come from ClipsterDCI 5.10.0.5.
155                         */
156                         if (notes) {
157                                 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
158                         }
159                         continue;
160                 }
161
162                 if (!boost::filesystem::exists(path)) {
163                         if (notes) {
164                                 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
165                         }
166                         continue;
167                 }
168
169                 /* Find the <Type> for this asset from the PKL that contains the asset */
170                 optional<string> pkl_type;
171                 for (auto j: _pkls) {
172                         pkl_type = j->type(i.first);
173                         if (pkl_type) {
174                                 break;
175                         }
176                 }
177
178                 if (!pkl_type) {
179                         /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
180                          * need to worry about it.
181                          */
182                         continue;
183                 }
184
185                 auto remove_parameters = [](string const& n) {
186                         return n.substr(0, n.find(";"));
187                 };
188
189                 /* Remove any optional parameters (after ;) */
190                 pkl_type = pkl_type->substr(0, pkl_type->find(";"));
191
192                 if (
193                         pkl_type == remove_parameters(CPL::static_pkl_type(standard)) ||
194                         pkl_type == remove_parameters(InteropSubtitleAsset::static_pkl_type(standard))) {
195                         auto p = new xmlpp::DomParser;
196                         try {
197                                 p->parse_file (path.string());
198                         } catch (std::exception& e) {
199                                 delete p;
200                                 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
201                         }
202
203                         auto const root = p->get_document()->get_root_node()->get_name();
204                         delete p;
205
206                         if (root == "CompositionPlaylist") {
207                                 auto cpl = make_shared<CPL>(path);
208                                 if (cpl->standard() != standard && notes) {
209                                         notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
210                                 }
211                                 _cpls.push_back (cpl);
212                         } else if (root == "DCSubtitle") {
213                                 if (standard == Standard::SMPTE && notes) {
214                                         notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
215                                 }
216                                 other_assets.push_back (make_shared<InteropSubtitleAsset>(path));
217                         }
218                 } else if (
219                         *pkl_type == remove_parameters(PictureAsset::static_pkl_type(standard)) ||
220                         *pkl_type == remove_parameters(SoundAsset::static_pkl_type(standard)) ||
221                         *pkl_type == remove_parameters(AtmosAsset::static_pkl_type(standard)) ||
222                         *pkl_type == remove_parameters(SMPTESubtitleAsset::static_pkl_type(standard))
223                         ) {
224
225                         bool found_threed_marked_as_twod = false;
226                         other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type, &found_threed_marked_as_twod));
227                         if (found_threed_marked_as_twod && notes) {
228                                 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, path});
229                         }
230                 } else if (*pkl_type == remove_parameters(FontAsset::static_pkl_type(standard))) {
231                         other_assets.push_back (make_shared<FontAsset>(i.first, path));
232                 } else if (*pkl_type == "image/png") {
233                         /* It's an Interop PNG subtitle; let it go */
234                 } else {
235                         throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
236                 }
237         }
238
239         resolve_refs (other_assets);
240
241         /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
242         if (notes) {
243                 for (auto i: cpls()) {
244                         for (auto j: i->reel_file_assets()) {
245                                 if (!j->asset_ref().resolved() && ids_and_paths.find(j->asset_ref().id()) == ids_and_paths.end()) {
246                                         notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
247                                 }
248                         }
249                 }
250         }
251 }
252
253
254 void
255 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
256 {
257         for (auto i: cpls()) {
258                 i->resolve_refs (assets);
259         }
260 }
261
262
263 bool
264 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
265 {
266         auto a = cpls ();
267         auto b = other.cpls ();
268
269         if (a.size() != b.size()) {
270                 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
271                 return false;
272         }
273
274         bool r = true;
275
276         for (auto i: a) {
277                 auto j = b.begin();
278                 while (j != b.end() && !(*j)->equals (i, opt, note)) {
279                         ++j;
280                 }
281
282                 if (j == b.end ()) {
283                         r = false;
284                 }
285         }
286
287         return r;
288 }
289
290
291 void
292 DCP::add (shared_ptr<CPL> cpl)
293 {
294         _cpls.push_back (cpl);
295 }
296
297
298 bool
299 DCP::any_encrypted () const
300 {
301         for (auto i: cpls()) {
302                 if (i->any_encrypted()) {
303                         return true;
304                 }
305         }
306
307         return false;
308 }
309
310
311 bool
312 DCP::all_encrypted () const
313 {
314         for (auto i: cpls()) {
315                 if (!i->all_encrypted()) {
316                         return false;
317                 }
318         }
319
320         return true;
321 }
322
323
324 void
325 DCP::add (DecryptedKDM const & kdm)
326 {
327         auto keys = kdm.keys();
328         for (auto cpl: cpls()) {
329                 if (std::any_of(keys.begin(), keys.end(), [cpl](DecryptedKDMKey const& key) { return key.cpl_id() == cpl->id(); })) {
330                         cpl->add (kdm);
331                 }
332         }
333 }
334
335
336 /** Write the VOLINDEX file.
337  *  @param standard DCP standard to use (INTEROP or SMPTE)
338  */
339 void
340 DCP::write_volindex (Standard standard) const
341 {
342         auto p = _directory;
343         switch (standard) {
344         case Standard::INTEROP:
345                 p /= "VOLINDEX";
346                 break;
347         case Standard::SMPTE:
348                 p /= "VOLINDEX.xml";
349                 break;
350         default:
351                 DCP_ASSERT (false);
352         }
353
354         xmlpp::Document doc;
355         xmlpp::Element* root;
356
357         switch (standard) {
358         case Standard::INTEROP:
359                 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
360                 break;
361         case Standard::SMPTE:
362                 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
363                 break;
364         default:
365                 DCP_ASSERT (false);
366         }
367
368         root->add_child("Index")->add_child_text ("1");
369         doc.write_to_file_formatted (p.string (), "UTF-8");
370 }
371
372
373 void
374 DCP::write_xml (
375         string issuer,
376         string creator,
377         string issue_date,
378         string annotation_text,
379         shared_ptr<const CertificateChain> signer,
380         NameFormat name_format
381         )
382 {
383         if (_cpls.empty()) {
384                 throw MiscError ("Cannot write DCP with no CPLs.");
385         }
386
387         auto standard = std::accumulate (
388                 std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
389                 [](Standard s, shared_ptr<CPL> c) {
390                         if (s != c->standard()) {
391                                 throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
392                         }
393                         return s;
394                 }
395                 );
396
397         for (auto i: cpls()) {
398                 NameFormat::Map values;
399                 values['t'] = "cpl";
400                 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer);
401         }
402
403         if (_pkls.empty()) {
404                 _pkls.push_back(make_shared<PKL>(standard, annotation_text, issue_date, issuer, creator));
405         }
406
407         auto pkl = _pkls.front();
408
409         /* The assets may have changed since we read the PKL, so re-add them */
410         pkl->clear_assets();
411         for (auto asset: assets()) {
412                 asset->add_to_pkl(pkl, _directory);
413         }
414
415         NameFormat::Map values;
416         values['t'] = "pkl";
417         auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
418         pkl->write (pkl_path, signer);
419
420         if (!_asset_map) {
421                 _asset_map = AssetMap(standard, annotation_text, issue_date, issuer, creator);
422         }
423
424         /* The assets may have changed since we read the asset map, so re-add them */
425         _asset_map->clear_assets();
426         _asset_map->add_asset(pkl->id(), pkl_path, true);
427         for (auto asset: assets()) {
428                 asset->add_to_assetmap(*_asset_map, _directory);
429         }
430
431         _asset_map->write_xml(
432                 _directory / (standard == Standard::INTEROP ? "ASSETMAP" : "ASSETMAP.xml")
433                 );
434
435         write_volindex (standard);
436 }
437
438
439 vector<shared_ptr<CPL>>
440 DCP::cpls () const
441 {
442         return _cpls;
443 }
444
445
446 vector<shared_ptr<Asset>>
447 DCP::assets (bool ignore_unresolved) const
448 {
449         vector<shared_ptr<Asset>> assets;
450         for (auto i: cpls()) {
451                 assets.push_back (i);
452                 for (auto j: i->reel_file_assets()) {
453                         if (ignore_unresolved && !j->asset_ref().resolved()) {
454                                 continue;
455                         }
456
457                         auto const id = j->asset_ref().id();
458                         auto already_got = false;
459                         for (auto k: assets) {
460                                 if (k->id() == id) {
461                                         already_got = true;
462                                 }
463                         }
464
465                         if (!already_got) {
466                                 auto o = j->asset_ref().asset();
467                                 assets.push_back (o);
468                                 /* More Interop special-casing */
469                                 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
470                                 if (sub) {
471                                         sub->add_font_assets (assets);
472                                 }
473                         }
474                 }
475         }
476
477         return assets;
478 }
479
480
481 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
482 vector<boost::filesystem::path>
483 DCP::directories_from_files (vector<boost::filesystem::path> files)
484 {
485         vector<boost::filesystem::path> d;
486         for (auto i: files) {
487                 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
488                         d.push_back (i.parent_path ());
489                 }
490         }
491         return d;
492 }