2 Copyright (C) 2012-2020 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.
38 #include "raw_convert.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"
50 #include "exceptions.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"
60 #include "asset_factory.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 #include <boost/foreach.hpp>
78 using boost::shared_ptr;
79 using boost::dynamic_pointer_cast;
80 using boost::optional;
81 using boost::algorithm::starts_with;
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";
89 DCP::DCP (boost::filesystem::path directory)
90 : _directory (directory)
92 if (!boost::filesystem::exists (directory)) {
93 boost::filesystem::create_directories (directory);
96 _directory = boost::filesystem::canonical (_directory);
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.
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.
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.
111 DCP::read (list<dcp::VerificationNote>* notes, bool ignore_incorrect_picture_mxf_type)
113 /* Read the ASSETMAP and PKL */
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";
120 boost::throw_exception (ReadError(String::compose("Could not find ASSETMAP nor ASSETMAP.xml in '%1'", _directory.string())));
123 cxml::Document asset_map ("AssetMap");
125 asset_map.read_file (_asset_map.get());
126 if (asset_map.namespace_uri() == assetmap_interop_ns) {
128 } else if (asset_map.namespace_uri() == assetmap_smpte_ns) {
131 boost::throw_exception (XMLError ("Unrecognised Assetmap namespace " + asset_map.namespace_uri()));
134 list<shared_ptr<cxml::Node> > 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 BOOST_FOREACH (shared_ptr<cxml::Node> i, asset_nodes) {
138 if (i->node_child("ChunkList")->node_children("Chunk").size() != 1) {
139 boost::throw_exception (XMLError ("unsupported asset chunk count"));
141 string p = i->node_child("ChunkList")->node_child("Chunk")->string_child ("Path");
142 if (starts_with (p, "file://")) {
145 switch (*_standard) {
147 if (i->optional_node_child("PackingList")) {
148 pkl_paths.push_back (p);
150 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
155 optional<string> pkl_bool = i->optional_string_child("PackingList");
156 if (pkl_bool && *pkl_bool == "true") {
157 pkl_paths.push_back (p);
159 paths.insert (make_pair (remove_urn_uuid (i->string_child ("Id")), p));
166 if (pkl_paths.empty()) {
167 boost::throw_exception (XMLError ("No packing lists found in asset map"));
170 BOOST_FOREACH (boost::filesystem::path i, pkl_paths) {
171 _pkls.push_back (shared_ptr<PKL>(new PKL(_directory / i)));
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.
178 Read all the assets from the asset map.
181 /* Make a list of non-CPL/PKL assets so that we can resolve the references
184 list<shared_ptr<Asset> > other_assets;
186 for (map<string, boost::filesystem::path>::const_iterator i = paths.begin(); i != paths.end(); ++i) {
187 boost::filesystem::path path = _directory / i->second;
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.
195 notes->push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::EMPTY_ASSET_PATH));
200 if (!boost::filesystem::exists(path)) {
202 notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISSING_ASSET, path));
207 /* Find the <Type> for this asset from the PKL that contains the asset */
208 optional<string> pkl_type;
209 BOOST_FOREACH (shared_ptr<PKL> j, _pkls) {
210 pkl_type = j->type(i->first);
216 DCP_ASSERT (pkl_type);
218 if (*pkl_type == CPL::static_pkl_type(*_standard) || *pkl_type == InteropSubtitleAsset::static_pkl_type(*_standard)) {
219 xmlpp::DomParser* p = new xmlpp::DomParser;
221 p->parse_file (path.string());
222 } catch (std::exception& e) {
224 throw ReadError(String::compose("XML error in %1", path.string()), e.what());
227 string const root = p->get_document()->get_root_node()->get_name ();
230 if (root == "CompositionPlaylist") {
231 shared_ptr<CPL> cpl (new CPL (path));
232 if (_standard && cpl->standard() && cpl->standard().get() != _standard.get() && notes) {
233 notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
235 _cpls.push_back (cpl);
236 } else if (root == "DCSubtitle") {
237 if (_standard && _standard.get() == SMPTE && notes) {
238 notes->push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISMATCHED_STANDARD));
240 other_assets.push_back (shared_ptr<InteropSubtitleAsset> (new InteropSubtitleAsset (path)));
243 *pkl_type == PictureAsset::static_pkl_type(*_standard) ||
244 *pkl_type == SoundAsset::static_pkl_type(*_standard) ||
245 *pkl_type == AtmosAsset::static_pkl_type(*_standard) ||
246 *pkl_type == SMPTESubtitleAsset::static_pkl_type(*_standard)
249 other_assets.push_back (asset_factory(path, ignore_incorrect_picture_mxf_type));
250 } else if (*pkl_type == FontAsset::static_pkl_type(*_standard)) {
251 other_assets.push_back (shared_ptr<FontAsset> (new FontAsset (i->first, path)));
252 } else if (*pkl_type == "image/png") {
253 /* It's an Interop PNG subtitle; let it go */
255 throw ReadError (String::compose("Unknown asset type %1 in PKL", *pkl_type));
259 resolve_refs (other_assets);
261 /* While we've got the ASSETMAP lets look and see if this DCP refers to things that are not in its ASSETMAP */
263 BOOST_FOREACH (shared_ptr<CPL> i, cpls()) {
264 BOOST_FOREACH (shared_ptr<const ReelMXF> j, i->reel_mxfs()) {
265 if (!j->asset_ref().resolved() && paths.find(j->asset_ref().id()) == paths.end()) {
266 notes->push_back (VerificationNote(VerificationNote::VERIFY_WARNING, VerificationNote::EXTERNAL_ASSET, j->asset_ref().id()));
274 DCP::resolve_refs (list<shared_ptr<Asset> > assets)
276 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
277 i->resolve_refs (assets);
282 DCP::equals (DCP const & other, EqualityOptions opt, NoteHandler note) const
284 list<shared_ptr<CPL> > a = cpls ();
285 list<shared_ptr<CPL> > b = other.cpls ();
287 if (a.size() != b.size()) {
288 note (DCP_ERROR, String::compose ("CPL counts differ: %1 vs %2", a.size(), b.size()));
294 BOOST_FOREACH (shared_ptr<CPL> i, a) {
295 list<shared_ptr<CPL> >::const_iterator j = b.begin ();
296 while (j != b.end() && !(*j)->equals (i, opt, note)) {
309 DCP::add (boost::shared_ptr<CPL> cpl)
311 _cpls.push_back (cpl);
315 DCP::encrypted () const
317 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
318 if (i->encrypted ()) {
326 /** Add a KDM to decrypt this DCP. This method must be called after DCP::read()
327 * or the KDM you specify will be ignored.
328 * @param kdm KDM to use.
331 DCP::add (DecryptedKDM const & kdm)
333 list<DecryptedKDMKey> keys = kdm.keys ();
335 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
336 BOOST_FOREACH (DecryptedKDMKey const & j, kdm.keys ()) {
337 if (j.cpl_id() == i->id()) {
344 /** Write the VOLINDEX file.
345 * @param standard DCP standard to use (INTEROP or SMPTE)
348 DCP::write_volindex (Standard standard) const
350 boost::filesystem::path p = _directory;
363 xmlpp::Element* root;
367 root = doc.create_root_node ("VolumeIndex", volindex_interop_ns);
370 root = doc.create_root_node ("VolumeIndex", volindex_smpte_ns);
376 root->add_child("Index")->add_child_text ("1");
377 doc.write_to_file_formatted (p.string (), "UTF-8");
381 DCP::write_assetmap (
382 Standard standard, string pkl_uuid, boost::filesystem::path pkl_path,
383 string issuer, string creator, string issue_date, string annotation_text
386 boost::filesystem::path p = _directory;
400 xmlpp::Element* root;
404 root = doc.create_root_node ("AssetMap", assetmap_interop_ns);
407 root = doc.create_root_node ("AssetMap", assetmap_smpte_ns);
413 root->add_child("Id")->add_child_text ("urn:uuid:" + make_uuid());
414 root->add_child("AnnotationText")->add_child_text (annotation_text);
418 root->add_child("VolumeCount")->add_child_text ("1");
419 root->add_child("IssueDate")->add_child_text (issue_date);
420 root->add_child("Issuer")->add_child_text (issuer);
421 root->add_child("Creator")->add_child_text (creator);
424 root->add_child("Creator")->add_child_text (creator);
425 root->add_child("VolumeCount")->add_child_text ("1");
426 root->add_child("IssueDate")->add_child_text (issue_date);
427 root->add_child("Issuer")->add_child_text (issuer);
433 xmlpp::Node* asset_list = root->add_child ("AssetList");
435 xmlpp::Node* asset = asset_list->add_child ("Asset");
436 asset->add_child("Id")->add_child_text ("urn:uuid:" + pkl_uuid);
437 asset->add_child("PackingList")->add_child_text ("true");
438 xmlpp::Node* chunk_list = asset->add_child ("ChunkList");
439 xmlpp::Node* chunk = chunk_list->add_child ("Chunk");
440 chunk->add_child("Path")->add_child_text (pkl_path.filename().string());
441 chunk->add_child("VolumeIndex")->add_child_text ("1");
442 chunk->add_child("Offset")->add_child_text ("0");
443 chunk->add_child("Length")->add_child_text (raw_convert<string> (boost::filesystem::file_size (pkl_path)));
445 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
446 i->write_to_assetmap (asset_list, _directory);
449 doc.write_to_file_formatted (p.string (), "UTF-8");
453 /** Write all the XML files for this DCP.
454 * @param standand INTEROP or SMPTE.
455 * @param metadata Metadata to use for PKL and asset map files.
456 * @param signer Signer to use, or 0.
464 string annotation_text,
465 shared_ptr<const CertificateChain> signer,
466 NameFormat name_format
469 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
470 NameFormat::Map values;
472 i->write_xml (_directory / (name_format.get(values, "_" + i->id() + ".xml")), standard, signer);
478 pkl.reset (new PKL(standard, annotation_text, issue_date, issuer, creator));
479 _pkls.push_back (pkl);
480 BOOST_FOREACH (shared_ptr<Asset> i, assets ()) {
481 i->add_to_pkl (pkl, _directory);
484 pkl = _pkls.front ();
487 NameFormat::Map values;
489 boost::filesystem::path pkl_path = _directory / name_format.get(values, "_" + pkl->id() + ".xml");
490 pkl->write (pkl_path, signer);
492 write_volindex (standard);
493 write_assetmap (standard, pkl->id(), pkl_path, issuer, creator, issue_date, annotation_text);
496 list<shared_ptr<CPL> >
502 /** @param ignore_unresolved true to silently ignore unresolved assets, otherwise
503 * an exception is thrown if they are found.
504 * @return All assets (including CPLs).
506 list<shared_ptr<Asset> >
507 DCP::assets (bool ignore_unresolved) const
509 list<shared_ptr<Asset> > assets;
510 BOOST_FOREACH (shared_ptr<CPL> i, cpls ()) {
511 assets.push_back (i);
512 BOOST_FOREACH (shared_ptr<const ReelMXF> j, i->reel_mxfs()) {
513 if (ignore_unresolved && !j->asset_ref().resolved()) {
517 string const id = j->asset_ref().id();
518 bool already_got = false;
519 BOOST_FOREACH (shared_ptr<Asset> k, assets) {
526 shared_ptr<Asset> o = j->asset_ref().asset();
527 assets.push_back (o);
528 /* More Interop special-casing */
529 shared_ptr<InteropSubtitleAsset> sub = dynamic_pointer_cast<InteropSubtitleAsset> (o);
531 sub->add_font_assets (assets);
540 /** Given a list of files that make up 1 or more DCPs, return the DCP directories */
541 vector<boost::filesystem::path>
542 DCP::directories_from_files (vector<boost::filesystem::path> files)
544 vector<boost::filesystem::path> d;
545 BOOST_FOREACH (boost::filesystem::path i, files) {
546 if (i.filename() == "ASSETMAP" || i.filename() == "ASSETMAP.xml") {
547 d.push_back (i.parent_path ());