2 Copyright (C) 2012-2021 Carl Hetherington <cth@carlh.net>
4 This file is part of libdcp.
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.
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.
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/>.
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
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.
40 #include "asset_factory.h"
41 #include "atmos_asset.h"
42 #include "certificate_chain.h"
43 #include "compose.hpp"
46 #include "dcp_assert.h"
47 #include "decrypted_kdm.h"
48 #include "decrypted_kdm_key.h"
49 #include "exceptions.h"
50 #include "filesystem.h"
51 #include "font_asset.h"
52 #include "interop_subtitle_asset.h"
54 #include "mono_j2k_picture_asset.h"
55 #include "mono_mpeg2_picture_asset.h"
56 #include "j2k_picture_asset.h"
58 #include "raw_convert.h"
59 #include "reel_asset.h"
60 #include "reel_subtitle_asset.h"
61 #include "smpte_subtitle_asset.h"
62 #include "sound_asset.h"
63 #include "stereo_j2k_picture_asset.h"
67 LIBDCP_DISABLE_WARNINGS
68 #include <asdcp/AS_DCP.h>
69 LIBDCP_ENABLE_WARNINGS
70 #include <xmlsec/xmldsig.h>
71 #include <xmlsec/app.h>
72 LIBDCP_DISABLE_WARNINGS
73 #include <libxml++/libxml++.h>
74 LIBDCP_ENABLE_WARNINGS
75 #include <boost/algorithm/string.hpp>
81 using std::dynamic_pointer_cast;
85 using std::make_shared;
87 using std::shared_ptr;
90 using boost::algorithm::starts_with;
91 using boost::optional;
95 static string const volindex_interop_ns = "http://www.digicine.com/PROTO-ASDCP-VL-20040311#";
96 static string const volindex_smpte_ns = "http://www.smpte-ra.org/schemas/429-9/2007/AM";
99 DCP::DCP (boost::filesystem::path directory)
100 : _directory (directory)
102 if (!filesystem::exists(directory)) {
103 filesystem::create_directories(directory);
106 _directory = filesystem::canonical(_directory);
110 DCP::DCP(DCP&& other)
111 : _directory(std::move(other._directory))
112 , _cpls(std::move(other._cpls))
113 , _pkls(std::move(other._pkls))
114 , _asset_map(std::move(other._asset_map))
115 , _new_issuer(std::move(other._new_issuer))
116 , _new_creator(std::move(other._new_creator))
117 , _new_issue_date(std::move(other._new_issue_date))
118 , _new_annotation_text(std::move(other._new_annotation_text))
125 DCP::operator=(DCP&& other)
127 _directory = std::move(other._directory);
128 _cpls = std::move(other._cpls);
129 _pkls = std::move(other._pkls);
130 _asset_map = std::move(other._asset_map);
131 _new_issuer = std::move(other._new_issuer);
132 _new_creator = std::move(other._new_creator);
133 _new_issue_date = std::move(other._new_issue_date);
134 _new_annotation_text = std::move(other._new_annotation_text);
140 DCP::read (vector<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
142 /* Read the ASSETMAP and PKL */
144 boost::filesystem::path asset_map_path;
145 if (filesystem::exists(_directory / "ASSETMAP")) {
146 asset_map_path = _directory / "ASSETMAP";
147 } else if (filesystem::exists(_directory / "ASSETMAP.xml")) {
148 asset_map_path = _directory / "ASSETMAP.xml";
150 boost::throw_exception(MissingAssetmapError(_directory));
153 _asset_map = AssetMap(asset_map_path);
154 auto const pkl_paths = _asset_map->pkl_paths();
155 auto const standard = _asset_map->standard();
157 if (pkl_paths.empty()) {
158 boost::throw_exception (XMLError ("No packing lists found in asset map"));
161 for (auto i: pkl_paths) {
162 _pkls.push_back(make_shared<PKL>(i));
166 paths - map of files in the DCP that are not PKLs; key is ID, value is path.
167 _pkls - PKL objects for each PKL.
169 Read all the assets from the asset map.
172 /* Make a list of non-CPL/PKL assets so that we can resolve the references
175 vector<shared_ptr<Asset>> other_assets;
177 auto ids_and_paths = _asset_map->asset_ids_and_paths();
178 for (auto id_and_path: ids_and_paths) {
179 auto const id = id_and_path.first;
180 auto const path = id_and_path.second;
182 if (path == _directory) {
183 /* I can't see how this is valid, but it's
184 been seen in the wild with a DCP that
185 claims to come from ClipsterDCI 5.10.0.5.
188 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
193 if (!filesystem::exists(path)) {
195 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
200 /* Find the <Type> for this asset from the PKL that contains the asset */
201 optional<string> pkl_type;
202 for (auto j: _pkls) {
203 pkl_type = j->type(id);
210 /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
211 * need to worry about it.
216 auto remove_parameters = [](string const& n) {
217 return n.substr(0, n.find(";"));
220 /* Remove any optional parameters (after ;) */
221 pkl_type = pkl_type->substr(0, pkl_type->find(";"));
224 pkl_type == remove_parameters(CPL::static_pkl_type(standard)) ||
225 pkl_type == remove_parameters(InteropSubtitleAsset::static_pkl_type(standard))) {
226 auto p = new xmlpp::DomParser;
228 p->parse_file(dcp::filesystem::fix_long_path(path).string());
229 } catch (std::exception& e) {
231 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
234 auto const root = p->get_document()->get_root_node()->get_name();
237 if (root == "CompositionPlaylist") {
238 auto cpl = make_shared<CPL>(path, notes);
239 if (cpl->standard() != standard && notes) {
240 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
242 _cpls.push_back (cpl);
243 } else if (root == "DCSubtitle") {
244 if (standard == Standard::SMPTE && notes) {
245 notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
247 other_assets.push_back (make_shared<InteropSubtitleAsset>(path));
250 *pkl_type == remove_parameters(J2KPictureAsset::static_pkl_type(standard)) ||
251 *pkl_type == remove_parameters(MPEG2PictureAsset::static_pkl_type(standard)) ||
252 *pkl_type == remove_parameters(SoundAsset::static_pkl_type(standard)) ||
253 *pkl_type == remove_parameters(AtmosAsset::static_pkl_type(standard)) ||
254 *pkl_type == remove_parameters(SMPTESubtitleAsset::static_pkl_type(standard))
257 bool found_threed_marked_as_twod = false;
258 auto asset = asset_factory(path, ignore_incorrect_picture_mxf_type, &found_threed_marked_as_twod);
259 if (asset->id() != id) {
260 notes->push_back(VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_ASSET_MAP_ID).set_id(id).set_other_id(asset->id()));
262 other_assets.push_back(asset);
263 if (found_threed_marked_as_twod && notes) {
264 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, path});
266 } else if (*pkl_type == remove_parameters(FontAsset::static_pkl_type(standard))) {
267 other_assets.push_back(make_shared<FontAsset>(id, path));
268 } else if (*pkl_type == "image/png") {
269 /* It's an Interop PNG subtitle; let it go */
271 throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
275 /* Set hashes for assets where we have an idea of what the hash should be in either a CPL or PKL.
276 * This means that when the hash is later read from these objects the result will be the one that
277 * it should be, rather the one that it currently is. This should prevent errors being concealed
278 * when an asset is corrupted - the hash from the CPL/PKL will disagree with the actual hash of the
279 * file, revealing the problem.
282 auto hash_from_pkl = [this](string id) -> optional<string> {
283 for (auto pkl: _pkls) {
284 if (auto pkl_hash = pkl->hash(id)) {
292 auto hash_from_cpl_or_pkl = [this, &hash_from_pkl](string id) -> optional<string> {
293 for (auto cpl: cpls()) {
294 for (auto reel_file_asset: cpl->reel_file_assets()) {
295 if (reel_file_asset->asset_ref().id() == id && reel_file_asset->hash()) {
296 return reel_file_asset->hash();
301 return hash_from_pkl(id);
304 for (auto asset: other_assets) {
305 if (auto hash = hash_from_cpl_or_pkl(asset->id())) {
306 asset->set_hash(*hash);
310 for (auto cpl: cpls()) {
311 if (auto hash = hash_from_pkl(cpl->id())) {
312 cpl->set_hash(*hash);
316 /* Resolve references */
317 resolve_refs (other_assets);
319 /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
321 for (auto i: cpls()) {
322 for (auto j: i->reel_file_assets()) {
323 if (!j->asset_ref().resolved() && ids_and_paths.find(j->asset_ref().id()) == ids_and_paths.end()) {
324 notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
333 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
335 for (auto i: cpls()) {
336 i->resolve_refs (assets);
342 DCP::equals(DCP const & other, EqualityOptions const& opt, NoteHandler note) const
345 auto b = other.cpls ();
347 if (a.size() != b.size()) {
348 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
356 while (j != b.end() && !(*j)->equals (i, opt, note)) {
370 DCP::add (shared_ptr<CPL> cpl)
372 _cpls.push_back (cpl);
377 DCP::any_encrypted () const
379 for (auto i: cpls()) {
380 if (i->any_encrypted()) {
390 DCP::all_encrypted () const
392 for (auto i: cpls()) {
393 if (!i->all_encrypted()) {
403 DCP::add (DecryptedKDM const & kdm)
405 auto keys = kdm.keys();
406 for (auto cpl: cpls()) {
407 if (std::any_of(keys.begin(), keys.end(), [cpl](DecryptedKDMKey const& key) { return key.cpl_id() == cpl->id(); })) {
414 /** Write the VOLINDEX file.
415 * @param standard DCP standard to use (INTEROP or SMPTE)
418 DCP::write_volindex (Standard standard) const
422 case Standard::INTEROP:
425 case Standard::SMPTE:
433 xmlpp::Element* root;
436 case Standard::INTEROP:
437 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
439 case Standard::SMPTE:
440 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
446 cxml::add_text_child(root, "Index", "1");
447 doc.write_to_file_formatted(dcp::filesystem::fix_long_path(p).string(), "UTF-8");
452 DCP::write_xml(shared_ptr<const CertificateChain> signer, bool include_mca_subdescriptors, NameFormat name_format)
455 throw MiscError ("Cannot write DCP with no CPLs.");
458 auto standard = std::accumulate (
459 std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
460 [](Standard s, shared_ptr<CPL> c) {
461 if (s != c->standard()) {
462 throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
468 for (auto i: cpls()) {
469 NameFormat::Map values;
471 i->write_xml(_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer, include_mca_subdescriptors);
478 _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
479 _new_issue_date.get_value_or(LocalTime().as_string()),
480 _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
481 _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
486 auto pkl = _pkls.front();
488 /* The assets may have changed since we read the PKL, so re-add them */
490 for (auto asset: assets()) {
491 asset->add_to_pkl(pkl, _directory);
494 NameFormat::Map values;
496 auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
497 pkl->write_xml (pkl_path, signer);
500 _asset_map = AssetMap(
502 _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
503 _new_issue_date.get_value_or(LocalTime().as_string()),
504 _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
505 _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
509 /* The assets may have changed since we read the asset map, so re-add them */
510 _asset_map->clear_assets();
511 _asset_map->add_asset(pkl->id(), pkl_path, true);
512 for (auto asset: assets()) {
513 asset->add_to_assetmap(*_asset_map, _directory);
516 _asset_map->write_xml(
517 _directory / (standard == Standard::INTEROP ? "ASSETMAP" : "ASSETMAP.xml")
520 write_volindex (standard);
524 vector<shared_ptr<CPL>>
531 vector<shared_ptr<Asset>>
532 DCP::assets (bool ignore_unresolved) const
534 vector<shared_ptr<Asset>> assets;
535 for (auto i: cpls()) {
536 assets.push_back (i);
537 for (auto j: i->reel_file_assets()) {
538 if (ignore_unresolved && !j->asset_ref().resolved()) {
542 auto const id = j->asset_ref().id();
543 if (std::find_if(assets.begin(), assets.end(), [id](shared_ptr<Asset> asset) { return asset->id() == id; }) == assets.end()) {
544 auto o = j->asset_ref().asset();
545 assets.push_back (o);
546 /* More Interop special-casing */
547 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
549 add_to_container(assets, sub->font_assets());
559 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
560 vector<boost::filesystem::path>
561 DCP::directories_from_files (vector<boost::filesystem::path> files)
563 vector<boost::filesystem::path> d;
564 for (auto i: files) {
565 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
566 d.push_back (i.parent_path ());
574 DCP::set_issuer(string issuer)
576 for (auto pkl: _pkls) {
577 pkl->set_issuer(issuer);
580 _asset_map->set_issuer(issuer);
582 _new_issuer = issuer;
587 DCP::set_creator(string creator)
589 for (auto pkl: _pkls) {
590 pkl->set_creator(creator);
593 _asset_map->set_creator(creator);
595 _new_creator = creator;
600 DCP::set_issue_date(string issue_date)
602 for (auto pkl: _pkls) {
603 pkl->set_issue_date(issue_date);
606 _asset_map->set_issue_date(issue_date);
608 _new_issue_date = issue_date;
613 DCP::set_annotation_text(string annotation_text)
615 for (auto pkl: _pkls) {
616 pkl->set_annotation_text(annotation_text);
619 _asset_map->set_annotation_text(annotation_text);
621 _new_annotation_text = annotation_text;