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_picture_asset.h"
55 #include "picture_asset.h"
57 #include "raw_convert.h"
58 #include "reel_asset.h"
59 #include "reel_subtitle_asset.h"
60 #include "smpte_subtitle_asset.h"
61 #include "sound_asset.h"
62 #include "stereo_picture_asset.h"
66 LIBDCP_DISABLE_WARNINGS
67 #include <asdcp/AS_DCP.h>
68 LIBDCP_ENABLE_WARNINGS
69 #include <xmlsec/xmldsig.h>
70 #include <xmlsec/app.h>
71 LIBDCP_DISABLE_WARNINGS
72 #include <libxml++/libxml++.h>
73 LIBDCP_ENABLE_WARNINGS
74 #include <boost/algorithm/string.hpp>
80 using std::dynamic_pointer_cast;
84 using std::make_shared;
86 using std::shared_ptr;
89 using boost::algorithm::starts_with;
90 using boost::optional;
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";
98 DCP::DCP (boost::filesystem::path directory)
99 : _directory (directory)
101 if (!filesystem::exists(directory)) {
102 filesystem::create_directories(directory);
105 _directory = filesystem::canonical(_directory);
109 DCP::DCP(DCP&& other)
110 : _directory(std::move(other._directory))
111 , _cpls(std::move(other._cpls))
112 , _pkls(std::move(other._pkls))
113 , _asset_map(std::move(other._asset_map))
114 , _new_issuer(std::move(other._new_issuer))
115 , _new_creator(std::move(other._new_creator))
116 , _new_issue_date(std::move(other._new_issue_date))
117 , _new_annotation_text(std::move(other._new_annotation_text))
124 DCP::operator=(DCP&& other)
126 _directory = std::move(other._directory);
127 _cpls = std::move(other._cpls);
128 _pkls = std::move(other._pkls);
129 _asset_map = std::move(other._asset_map);
130 _new_issuer = std::move(other._new_issuer);
131 _new_creator = std::move(other._new_creator);
132 _new_issue_date = std::move(other._new_issue_date);
133 _new_annotation_text = std::move(other._new_annotation_text);
139 DCP::read (vector<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
141 /* Read the ASSETMAP and PKL */
143 boost::filesystem::path asset_map_path;
144 if (filesystem::exists(_directory / "ASSETMAP")) {
145 asset_map_path = _directory / "ASSETMAP";
146 } else if (filesystem::exists(_directory / "ASSETMAP.xml")) {
147 asset_map_path = _directory / "ASSETMAP.xml";
149 boost::throw_exception(MissingAssetmapError(_directory));
152 _asset_map = AssetMap(asset_map_path);
153 auto const pkl_paths = _asset_map->pkl_paths();
154 auto const standard = _asset_map->standard();
156 if (pkl_paths.empty()) {
157 boost::throw_exception (XMLError ("No packing lists found in asset map"));
160 for (auto i: pkl_paths) {
161 _pkls.push_back(make_shared<PKL>(i));
165 paths - map of files in the DCP that are not PKLs; key is ID, value is path.
166 _pkls - PKL objects for each PKL.
168 Read all the assets from the asset map.
171 /* Make a list of non-CPL/PKL assets so that we can resolve the references
174 vector<shared_ptr<Asset>> other_assets;
176 auto ids_and_paths = _asset_map->asset_ids_and_paths();
177 for (auto id_and_path: ids_and_paths) {
178 auto const id = id_and_path.first;
179 auto const path = id_and_path.second;
181 if (path == _directory) {
182 /* I can't see how this is valid, but it's
183 been seen in the wild with a DCP that
184 claims to come from ClipsterDCI 5.10.0.5.
187 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_ASSET_PATH});
192 if (!filesystem::exists(path)) {
194 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSET, path});
199 /* Find the <Type> for this asset from the PKL that contains the asset */
200 optional<string> pkl_type;
201 for (auto j: _pkls) {
202 pkl_type = j->type(id);
209 /* This asset is in the ASSETMAP but not mentioned in any PKL so we don't
210 * need to worry about it.
215 auto remove_parameters = [](string const& n) {
216 return n.substr(0, n.find(";"));
219 /* Remove any optional parameters (after ;) */
220 pkl_type = pkl_type->substr(0, pkl_type->find(";"));
223 pkl_type == remove_parameters(CPL::static_pkl_type(standard)) ||
224 pkl_type == remove_parameters(InteropSubtitleAsset::static_pkl_type(standard))) {
225 auto p = new xmlpp::DomParser;
227 p->parse_file(dcp::filesystem::fix_long_path(path).string());
228 } catch (std::exception& e) {
230 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
233 auto const root = p->get_document()->get_root_node()->get_name();
236 if (root == "CompositionPlaylist") {
237 auto cpl = make_shared<CPL>(path);
238 if (cpl->standard() != standard && notes) {
239 notes->push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD});
241 _cpls.push_back (cpl);
242 } else if (root == "DCSubtitle") {
243 if (standard == Standard::SMPTE && notes) {
244 notes->push_back (VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_STANDARD));
246 other_assets.push_back (make_shared<InteropSubtitleAsset>(path));
249 *pkl_type == remove_parameters(PictureAsset::static_pkl_type(standard)) ||
250 *pkl_type == remove_parameters(SoundAsset::static_pkl_type(standard)) ||
251 *pkl_type == remove_parameters(AtmosAsset::static_pkl_type(standard)) ||
252 *pkl_type == remove_parameters(SMPTESubtitleAsset::static_pkl_type(standard))
255 bool found_threed_marked_as_twod = false;
256 auto asset = asset_factory(path, ignore_incorrect_picture_mxf_type, &found_threed_marked_as_twod);
257 if (asset->id() != id) {
258 notes->push_back(VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_ASSET_MAP_ID).set_id(id).set_other_id(asset->id()));
260 other_assets.push_back(asset);
261 if (found_threed_marked_as_twod && notes) {
262 notes->push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, path});
264 } else if (*pkl_type == remove_parameters(FontAsset::static_pkl_type(standard))) {
265 other_assets.push_back(make_shared<FontAsset>(id, path));
266 } else if (*pkl_type == "image/png") {
267 /* It's an Interop PNG subtitle; let it go */
269 throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
273 /* Set hashes for assets where we have an idea of what the hash should be in either a CPL or PKL.
274 * This means that when the hash is later read from these objects the result will be the one that
275 * it should be, rather the one that it currently is. This should prevent errors being concealed
276 * when an asset is corrupted - the hash from the CPL/PKL will disagree with the actual hash of the
277 * file, revealing the problem.
280 auto hash_from_pkl = [this](string id) -> optional<string> {
281 for (auto pkl: _pkls) {
282 if (auto pkl_hash = pkl->hash(id)) {
290 auto hash_from_cpl_or_pkl = [this, &hash_from_pkl](string id) -> optional<string> {
291 for (auto cpl: cpls()) {
292 for (auto reel_file_asset: cpl->reel_file_assets()) {
293 if (reel_file_asset->asset_ref().id() == id && reel_file_asset->hash()) {
294 return reel_file_asset->hash();
299 return hash_from_pkl(id);
302 for (auto asset: other_assets) {
303 if (auto hash = hash_from_cpl_or_pkl(asset->id())) {
304 asset->set_hash(*hash);
308 for (auto cpl: cpls()) {
309 if (auto hash = hash_from_pkl(cpl->id())) {
310 cpl->set_hash(*hash);
314 /* Resolve references */
315 resolve_refs (other_assets);
317 /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
319 for (auto i: cpls()) {
320 for (auto j: i->reel_file_assets()) {
321 if (!j->asset_ref().resolved() && ids_and_paths.find(j->asset_ref().id()) == ids_and_paths.end()) {
322 notes->push_back (VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EXTERNAL_ASSET, j->asset_ref().id()));
331 DCP::resolve_refs (vector<shared_ptr<Asset>> assets)
333 for (auto i: cpls()) {
334 i->resolve_refs (assets);
340 DCP::equals(DCP const & other, EqualityOptions const& opt, NoteHandler note) const
343 auto b = other.cpls ();
345 if (a.size() != b.size()) {
346 note (NoteType::ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
354 while (j != b.end() && !(*j)->equals (i, opt, note)) {
368 DCP::add (shared_ptr<CPL> cpl)
370 _cpls.push_back (cpl);
375 DCP::any_encrypted () const
377 for (auto i: cpls()) {
378 if (i->any_encrypted()) {
388 DCP::all_encrypted () const
390 for (auto i: cpls()) {
391 if (!i->all_encrypted()) {
401 DCP::add (DecryptedKDM const & kdm)
403 auto keys = kdm.keys();
404 for (auto cpl: cpls()) {
405 if (std::any_of(keys.begin(), keys.end(), [cpl](DecryptedKDMKey const& key) { return key.cpl_id() == cpl->id(); })) {
412 /** Write the VOLINDEX file.
413 * @param standard DCP standard to use (INTEROP or SMPTE)
416 DCP::write_volindex (Standard standard) const
420 case Standard::INTEROP:
423 case Standard::SMPTE:
431 xmlpp::Element* root;
434 case Standard::INTEROP:
435 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
437 case Standard::SMPTE:
438 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
444 cxml::add_text_child(root, "Index", "1");
445 doc.write_to_file_formatted(dcp::filesystem::fix_long_path(p).string(), "UTF-8");
450 DCP::write_xml(shared_ptr<const CertificateChain> signer, bool include_mca_subdescriptors, NameFormat name_format)
453 throw MiscError ("Cannot write DCP with no CPLs.");
456 auto standard = std::accumulate (
457 std::next(_cpls.begin()), _cpls.end(), _cpls[0]->standard(),
458 [](Standard s, shared_ptr<CPL> c) {
459 if (s != c->standard()) {
460 throw MiscError ("Cannot make DCP with mixed Interop and SMPTE CPLs.");
466 for (auto i: cpls()) {
467 NameFormat::Map values;
469 i->write_xml(_directory / (name_format.get(values, "_" + i->id() + ".xml")), signer, include_mca_subdescriptors);
476 _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
477 _new_issue_date.get_value_or(LocalTime().as_string()),
478 _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
479 _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
484 auto pkl = _pkls.front();
486 /* The assets may have changed since we read the PKL, so re-add them */
488 for (auto asset: assets()) {
489 asset->add_to_pkl(pkl, _directory);
492 NameFormat::Map values;
494 auto pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
495 pkl->write_xml (pkl_path, signer);
498 _asset_map = AssetMap(
500 _new_annotation_text.get_value_or(String::compose("Created by libdcp %1", dcp::version)),
501 _new_issue_date.get_value_or(LocalTime().as_string()),
502 _new_issuer.get_value_or(String::compose("libdcp %1", dcp::version)),
503 _new_creator.get_value_or(String::compose("libdcp %1", dcp::version))
507 /* The assets may have changed since we read the asset map, so re-add them */
508 _asset_map->clear_assets();
509 _asset_map->add_asset(pkl->id(), pkl_path, true);
510 for (auto asset: assets()) {
511 asset->add_to_assetmap(*_asset_map, _directory);
514 _asset_map->write_xml(
515 _directory / (standard == Standard::INTEROP ? "ASSETMAP" : "ASSETMAP.xml")
518 write_volindex (standard);
522 vector<shared_ptr<CPL>>
529 vector<shared_ptr<Asset>>
530 DCP::assets (bool ignore_unresolved) const
532 vector<shared_ptr<Asset>> assets;
533 for (auto i: cpls()) {
534 assets.push_back (i);
535 for (auto j: i->reel_file_assets()) {
536 if (ignore_unresolved && !j->asset_ref().resolved()) {
540 auto const id = j->asset_ref().id();
541 if (std::find_if(assets.begin(), assets.end(), [id](shared_ptr<Asset> asset) { return asset->id() == id; }) == assets.end()) {
542 auto o = j->asset_ref().asset();
543 assets.push_back (o);
544 /* More Interop special-casing */
545 auto sub = dynamic_pointer_cast<InteropSubtitleAsset>(o);
547 add_to_container(assets, sub->font_assets());
557 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
558 vector<boost::filesystem::path>
559 DCP::directories_from_files (vector<boost::filesystem::path> files)
561 vector<boost::filesystem::path> d;
562 for (auto i: files) {
563 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
564 d.push_back (i.parent_path ());
572 DCP::set_issuer(string issuer)
574 for (auto pkl: _pkls) {
575 pkl->set_issuer(issuer);
578 _asset_map->set_issuer(issuer);
580 _new_issuer = issuer;
585 DCP::set_creator(string creator)
587 for (auto pkl: _pkls) {
588 pkl->set_creator(creator);
591 _asset_map->set_creator(creator);
593 _new_creator = creator;
598 DCP::set_issue_date(string issue_date)
600 for (auto pkl: _pkls) {
601 pkl->set_issue_date(issue_date);
604 _asset_map->set_issue_date(issue_date);
606 _new_issue_date = issue_date;
611 DCP::set_annotation_text(string annotation_text)
613 for (auto pkl: _pkls) {
614 pkl->set_annotation_text(annotation_text);
617 _asset_map->set_annotation_text(annotation_text);
619 _new_annotation_text = annotation_text;