summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCarl Hetherington <cth@carlh.net>2025-12-16 17:45:46 +0100
committerCarl Hetherington <cth@carlh.net>2025-12-18 01:10:32 +0100
commitc3c127e0bdb988696d16f47ea8080df3eff38420 (patch)
treeecdff8d299152eef491f1946e6ccb4f6d9cf8e71
parent45ed3bf55175a8555123b320b366efb69b1ba629 (diff)
Add can_be_read() to ReelFileAsset and subclasses.
-rw-r--r--src/atmos_asset.cc22
-rw-r--r--src/atmos_asset.h2
-rw-r--r--src/mono_j2k_picture_asset.cc22
-rw-r--r--src/mono_j2k_picture_asset.h2
-rw-r--r--src/mono_mpeg2_picture_asset.cc24
-rw-r--r--src/mono_mpeg2_picture_asset.h2
-rw-r--r--src/mxf.cc7
-rw-r--r--src/mxf.h2
-rw-r--r--src/reel_atmos_asset.cc12
-rw-r--r--src/reel_atmos_asset.h2
-rw-r--r--src/reel_file_asset.cc7
-rw-r--r--src/reel_file_asset.h1
-rw-r--r--src/reel_mono_picture_asset.cc14
-rw-r--r--src/reel_mono_picture_asset.h2
-rw-r--r--src/reel_smpte_text_asset.cc11
-rw-r--r--src/reel_smpte_text_asset.h2
-rw-r--r--src/reel_sound_asset.cc12
-rw-r--r--src/reel_sound_asset.h2
-rw-r--r--src/reel_stereo_picture_asset.cc17
-rw-r--r--src/reel_stereo_picture_asset.h2
-rw-r--r--src/smpte_text_asset.cc21
-rw-r--r--src/smpte_text_asset.h2
-rw-r--r--src/sound_asset.cc21
-rw-r--r--src/sound_asset.h2
-rw-r--r--src/stereo_j2k_picture_asset.cc22
-rw-r--r--src/stereo_j2k_picture_asset.h2
-rw-r--r--test/can_be_read_test.cc152
-rw-r--r--test/data/other_kdm.xml190
-rw-r--r--test/wscript1
29 files changed, 577 insertions, 3 deletions
diff --git a/src/atmos_asset.cc b/src/atmos_asset.cc
index 09a22c1e..9c4b1c80 100644
--- a/src/atmos_asset.cc
+++ b/src/atmos_asset.cc
@@ -122,3 +122,25 @@ AtmosAsset::start_write (boost::filesystem::path file)
/* Can't use make_shared here since the constructor is protected */
return shared_ptr<AtmosAssetWriter>(new AtmosAssetWriter(this, file));
}
+
+
+bool
+AtmosAsset::can_be_read() const
+{
+ if (!MXF::can_be_read()) {
+ return false;
+ }
+
+ try {
+ auto reader = start_read();
+ reader->set_check_hmac(false);
+ reader->get_frame(0);
+ } catch (dcp::ReadError&) {
+ return false;
+ } catch (dcp::MiscError&) {
+ return false;
+ }
+
+ return true;
+}
+
diff --git a/src/atmos_asset.h b/src/atmos_asset.h
index a682ae60..9fcd359f 100644
--- a/src/atmos_asset.h
+++ b/src/atmos_asset.h
@@ -61,6 +61,8 @@ public:
AtmosAsset (Fraction edit_rate, int first_frame, int max_channel_count, int max_object_count, int atmos_version);
explicit AtmosAsset (boost::filesystem::path file);
+ bool can_be_read() const override;
+
std::shared_ptr<AtmosAssetWriter> start_write (boost::filesystem::path file);
std::shared_ptr<AtmosAssetReader> start_read () const;
diff --git a/src/mono_j2k_picture_asset.cc b/src/mono_j2k_picture_asset.cc
index b1f311d6..4a8a3219 100644
--- a/src/mono_j2k_picture_asset.cc
+++ b/src/mono_j2k_picture_asset.cc
@@ -207,3 +207,25 @@ MonoJ2KPictureAsset::cpl_node_name () const
{
return "MainPicture";
}
+
+
+bool
+MonoJ2KPictureAsset::can_be_read() const
+{
+ if (!MXF::can_be_read()) {
+ return false;
+ }
+
+ try {
+ auto reader = start_read();
+ reader->set_check_hmac(false);
+ reader->get_frame(0)->xyz_image();
+ } catch (dcp::ReadError&) {
+ return false;
+ } catch (dcp::MiscError&) {
+ return false;
+ }
+
+ return true;
+}
+
diff --git a/src/mono_j2k_picture_asset.h b/src/mono_j2k_picture_asset.h
index d716b8ff..f790e543 100644
--- a/src/mono_j2k_picture_asset.h
+++ b/src/mono_j2k_picture_asset.h
@@ -68,6 +68,8 @@ public:
*/
MonoJ2KPictureAsset(Fraction edit_rate, Standard standard);
+ bool can_be_read() const override;
+
/** Start a progressive write to a MonoJ2KPictureAsset.
* @path file File to write to.
* @path behaviour OVERWRITE_EXISTING to overwrite and potentially add to an existing file
diff --git a/src/mono_mpeg2_picture_asset.cc b/src/mono_mpeg2_picture_asset.cc
index 380da0fe..9a879f0b 100644
--- a/src/mono_mpeg2_picture_asset.cc
+++ b/src/mono_mpeg2_picture_asset.cc
@@ -36,6 +36,7 @@
#include "mono_mpeg2_picture_asset.h"
#include "mono_mpeg2_picture_asset_reader.h"
#include "mono_mpeg2_picture_asset_writer.h"
+#include "mpeg2_transcode.h"
#include <asdcp/AS_DCP.h>
@@ -84,3 +85,26 @@ MonoMPEG2PictureAsset::start_write(boost::filesystem::path file, Behaviour behav
/* Can't use make_shared here as the MonoJ2KPictureAssetWriter constructor is private */
return shared_ptr<MonoMPEG2PictureAssetWriter>(new MonoMPEG2PictureAssetWriter(this, file, behaviour == Behaviour::OVERWRITE_EXISTING));
}
+
+
+bool
+MonoMPEG2PictureAsset::can_be_read() const
+{
+ if (!MXF::can_be_read()) {
+ return false;
+ }
+
+ try {
+ auto reader = start_read();
+ reader->set_check_hmac(false);
+ dcp::MPEG2Decompressor decompressor;
+ decompressor.decompress_frame(reader->get_frame(0));
+ } catch (dcp::ReadError&) {
+ return false;
+ } catch (dcp::MiscError&) {
+ return false;
+ }
+
+ return true;
+}
+
diff --git a/src/mono_mpeg2_picture_asset.h b/src/mono_mpeg2_picture_asset.h
index 8ef3653e..dc2637e7 100644
--- a/src/mono_mpeg2_picture_asset.h
+++ b/src/mono_mpeg2_picture_asset.h
@@ -61,6 +61,8 @@ public:
explicit MonoMPEG2PictureAsset(boost::filesystem::path file);
+ bool can_be_read() const override;
+
std::shared_ptr<MPEG2PictureAssetWriter> start_write(boost::filesystem::path file, Behaviour behaviour) override;
std::shared_ptr<MonoMPEG2PictureAssetReader> start_read() const;
};
diff --git a/src/mxf.cc b/src/mxf.cc
index 1154b80d..60ef5552 100644
--- a/src/mxf.cc
+++ b/src/mxf.cc
@@ -146,3 +146,10 @@ MXF::read_writer_info (ASDCP::WriterInfo const & info)
Kumu::bin2UUIDhex (info.AssetUUID, ASDCP::UUIDlen, buffer, sizeof (buffer));
return buffer;
}
+
+
+bool
+MXF::can_be_read() const
+{
+ return !encrypted() || key();
+}
diff --git a/src/mxf.h b/src/mxf.h
index ad9e39ed..933e7afb 100644
--- a/src/mxf.h
+++ b/src/mxf.h
@@ -76,6 +76,8 @@ public:
MXF (Standard standard);
virtual ~MXF () {}
+ virtual bool can_be_read() const;
+
/** @return true if the data is encrypted */
bool encrypted () const {
return static_cast<bool>(_key_id);
diff --git a/src/reel_atmos_asset.cc b/src/reel_atmos_asset.cc
index fcecb548..ae7be33a 100644
--- a/src/reel_atmos_asset.cc
+++ b/src/reel_atmos_asset.cc
@@ -104,3 +104,15 @@ ReelAtmosAsset::equals(shared_ptr<const ReelAtmosAsset> other, EqualityOptions c
return true;
}
+
+
+bool
+ReelAtmosAsset::can_be_read() const
+{
+ if (!ReelFileAsset::can_be_read()) {
+ return false;
+ }
+
+ return asset()->can_be_read();
+}
+
diff --git a/src/reel_atmos_asset.h b/src/reel_atmos_asset.h
index ab18d1ab..ed525bae 100644
--- a/src/reel_atmos_asset.h
+++ b/src/reel_atmos_asset.h
@@ -60,6 +60,8 @@ public:
ReelAtmosAsset (std::shared_ptr<AtmosAsset> asset, int64_t entry_point);
explicit ReelAtmosAsset (std::shared_ptr<const cxml::Node>);
+ bool can_be_read() const override;
+
std::shared_ptr<const AtmosAsset> asset () const {
return asset_of_type<const AtmosAsset>();
}
diff --git a/src/reel_file_asset.cc b/src/reel_file_asset.cc
index 8fed8012..ad534ebd 100644
--- a/src/reel_file_asset.cc
+++ b/src/reel_file_asset.cc
@@ -107,3 +107,10 @@ ReelFileAsset::write_to_cpl(xmlpp::Element* node, Standard standard) const
return asset;
}
+
+bool
+ReelFileAsset::can_be_read() const
+{
+ return asset_ref().resolved();
+}
+
diff --git a/src/reel_file_asset.h b/src/reel_file_asset.h
index 48fdf215..2412ac2b 100644
--- a/src/reel_file_asset.h
+++ b/src/reel_file_asset.h
@@ -57,6 +57,7 @@ public:
explicit ReelFileAsset (std::shared_ptr<const cxml::Node> node);
virtual xmlpp::Element* write_to_cpl(xmlpp::Element* node, Standard standard) const override;
+ virtual bool can_be_read() const;
/** @return a Ref to our actual asset */
Ref const & asset_ref () const {
diff --git a/src/reel_mono_picture_asset.cc b/src/reel_mono_picture_asset.cc
index 5dbf85d2..9cbeb7e1 100644
--- a/src/reel_mono_picture_asset.cc
+++ b/src/reel_mono_picture_asset.cc
@@ -42,8 +42,9 @@
#include <libcxml/cxml.h>
-using std::string;
+using std::dynamic_pointer_cast;
using std::shared_ptr;
+using std::string;
using namespace dcp;
@@ -66,3 +67,14 @@ ReelMonoPictureAsset::cpl_node_name() const
{
return "MainPicture";
}
+
+
+bool
+ReelMonoPictureAsset::can_be_read() const
+{
+ if (!ReelFileAsset::can_be_read()) {
+ return false;
+ }
+
+ return asset()->can_be_read();
+}
diff --git a/src/reel_mono_picture_asset.h b/src/reel_mono_picture_asset.h
index 0c468e0e..29df2d14 100644
--- a/src/reel_mono_picture_asset.h
+++ b/src/reel_mono_picture_asset.h
@@ -61,6 +61,8 @@ public:
ReelMonoPictureAsset(std::shared_ptr<PictureAsset> asset, int64_t entry_point);
explicit ReelMonoPictureAsset (std::shared_ptr<const cxml::Node>);
+ bool can_be_read() const override;
+
/** @return the MonoJ2KPictureAsset that this object refers to, if applicable */
std::shared_ptr<const MonoJ2KPictureAsset> mono_j2k_asset() const {
return asset_of_type<const MonoJ2KPictureAsset>();
diff --git a/src/reel_smpte_text_asset.cc b/src/reel_smpte_text_asset.cc
index b1ce34cb..30a4d443 100644
--- a/src/reel_smpte_text_asset.cc
+++ b/src/reel_smpte_text_asset.cc
@@ -116,3 +116,14 @@ ReelSMPTETextAsset::write_to_cpl(xmlpp::Element* node, Standard standard) const
}
+
+bool
+ReelSMPTETextAsset::can_be_read() const
+{
+ if (!ReelFileAsset::can_be_read()) {
+ return false;
+ }
+
+ return smpte_asset()->can_be_read();
+}
+
diff --git a/src/reel_smpte_text_asset.h b/src/reel_smpte_text_asset.h
index b3c2f655..68459c30 100644
--- a/src/reel_smpte_text_asset.h
+++ b/src/reel_smpte_text_asset.h
@@ -56,6 +56,8 @@ public:
ReelSMPTETextAsset(TextType type, std::shared_ptr<SMPTETextAsset> asset, Fraction edit_rate, int64_t intrinsic_duration, int64_t entry_point);
explicit ReelSMPTETextAsset(std::shared_ptr<const cxml::Node>);
+ bool can_be_read() const override;
+
std::shared_ptr<const SMPTETextAsset> smpte_asset() const {
return asset_of_type<const SMPTETextAsset>();
}
diff --git a/src/reel_sound_asset.cc b/src/reel_sound_asset.cc
index dbac6cb3..3c11f666 100644
--- a/src/reel_sound_asset.cc
+++ b/src/reel_sound_asset.cc
@@ -93,3 +93,15 @@ ReelSoundAsset::equals(shared_ptr<const ReelSoundAsset> other, EqualityOptions c
return true;
}
+
+
+bool
+ReelSoundAsset::can_be_read() const
+{
+ if (!ReelFileAsset::can_be_read()) {
+ return false;
+ }
+
+ return asset()->can_be_read();
+}
+
diff --git a/src/reel_sound_asset.h b/src/reel_sound_asset.h
index 6537ab5c..ac157bdb 100644
--- a/src/reel_sound_asset.h
+++ b/src/reel_sound_asset.h
@@ -55,6 +55,8 @@ public:
ReelSoundAsset (std::shared_ptr<dcp::SoundAsset> content, int64_t entry_point);
explicit ReelSoundAsset (std::shared_ptr<const cxml::Node>);
+ bool can_be_read() const override;
+
/** @return the SoundAsset that this object refers to */
std::shared_ptr<const SoundAsset> asset () const {
return asset_of_type<const SoundAsset>();
diff --git a/src/reel_stereo_picture_asset.cc b/src/reel_stereo_picture_asset.cc
index 2ee452d9..53abb8d6 100644
--- a/src/reel_stereo_picture_asset.cc
+++ b/src/reel_stereo_picture_asset.cc
@@ -42,10 +42,11 @@
#include <libcxml/cxml.h>
-using std::string;
-using std::pair;
+using std::dynamic_pointer_cast;
using std::make_pair;
+using std::pair;
using std::shared_ptr;
+using std::string;
using namespace dcp;
@@ -82,3 +83,15 @@ ReelStereoPictureAsset::cpl_node_attribute (Standard standard) const
DCP_ASSERT (false);
}
+
+
+bool
+ReelStereoPictureAsset::can_be_read() const
+{
+ if (!ReelFileAsset::can_be_read()) {
+ return false;
+ }
+
+ return asset()->can_be_read();
+}
+
diff --git a/src/reel_stereo_picture_asset.h b/src/reel_stereo_picture_asset.h
index 0dd726a4..9e5b30cf 100644
--- a/src/reel_stereo_picture_asset.h
+++ b/src/reel_stereo_picture_asset.h
@@ -60,6 +60,8 @@ public:
ReelStereoPictureAsset (std::shared_ptr<StereoJ2KPictureAsset> content, int64_t entry_point);
explicit ReelStereoPictureAsset (std::shared_ptr<const cxml::Node>);
+ bool can_be_read() const override;
+
/** @return the StereoJ2KPictureAsset that this object refers to */
std::shared_ptr<const StereoJ2KPictureAsset> stereo_asset () const {
return asset_of_type<const StereoJ2KPictureAsset>();
diff --git a/src/smpte_text_asset.cc b/src/smpte_text_asset.cc
index 63e8c8ab..84624171 100644
--- a/src/smpte_text_asset.cc
+++ b/src/smpte_text_asset.cc
@@ -615,3 +615,24 @@ SMPTETextAsset::schema_namespace() const
DCP_ASSERT(false);
}
+
+
+
+bool
+SMPTETextAsset::can_be_read() const
+{
+ if (!MXF::can_be_read()) {
+ return false;
+ }
+
+ try {
+ texts();
+ } catch (dcp::ReadError&) {
+ return false;
+ } catch (dcp::MiscError&) {
+ return false;
+ }
+
+ return true;
+}
+
diff --git a/src/smpte_text_asset.h b/src/smpte_text_asset.h
index 6a08ffe4..9b7f09a6 100644
--- a/src/smpte_text_asset.h
+++ b/src/smpte_text_asset.h
@@ -82,6 +82,8 @@ public:
*/
explicit SMPTETextAsset(boost::filesystem::path file);
+ bool can_be_read() const override;
+
bool equals (
std::shared_ptr<const Asset>,
EqualityOptions const&,
diff --git a/src/sound_asset.cc b/src/sound_asset.cc
index f671a58a..614f6165 100644
--- a/src/sound_asset.cc
+++ b/src/sound_asset.cc
@@ -294,3 +294,24 @@ SoundAsset::active_channels() const
return _active_channels.get_value_or(_channels);
}
+
+bool
+SoundAsset::can_be_read() const
+{
+ if (!MXF::can_be_read()) {
+ return false;
+ }
+
+ try {
+ auto reader = start_read();
+ reader->set_check_hmac(false);
+ reader->get_frame(0);
+ } catch (dcp::ReadError&) {
+ return false;
+ } catch (dcp::MiscError&) {
+ return false;
+ }
+
+ return true;
+}
+
diff --git a/src/sound_asset.h b/src/sound_asset.h
index e5acb119..64bc0a03 100644
--- a/src/sound_asset.h
+++ b/src/sound_asset.h
@@ -84,6 +84,8 @@ public:
DISABLED
};
+ bool can_be_read() const override;
+
/** @param extra_active_channels list of channels that are active in the asset, other than the basic 5.1
* which are assumed always to be active.
*/
diff --git a/src/stereo_j2k_picture_asset.cc b/src/stereo_j2k_picture_asset.cc
index 6a5e7d79..d403ed41 100644
--- a/src/stereo_j2k_picture_asset.cc
+++ b/src/stereo_j2k_picture_asset.cc
@@ -181,3 +181,25 @@ StereoJ2KPictureAsset::equals(shared_ptr<const Asset> other, EqualityOptions con
return result;
}
+
+
+bool
+StereoJ2KPictureAsset::can_be_read() const
+{
+ if (!MXF::can_be_read()) {
+ return false;
+ }
+
+ try {
+ auto reader = start_read();
+ reader->set_check_hmac(false);
+ reader->get_frame(0)->xyz_image(Eye::LEFT);
+ } catch (dcp::ReadError&) {
+ return false;
+ } catch (dcp::MiscError&) {
+ return false;
+ }
+
+ return true;
+}
+
diff --git a/src/stereo_j2k_picture_asset.h b/src/stereo_j2k_picture_asset.h
index 7ef86ec5..6c870d59 100644
--- a/src/stereo_j2k_picture_asset.h
+++ b/src/stereo_j2k_picture_asset.h
@@ -57,6 +57,8 @@ public:
explicit StereoJ2KPictureAsset (boost::filesystem::path file);
explicit StereoJ2KPictureAsset (Fraction edit_rate, Standard standard);
+ bool can_be_read() const override;
+
/** Start a progressive write to a StereoJ2KPictureAsset */
std::shared_ptr<J2KPictureAssetWriter> start_write(boost::filesystem::path file, Behaviour behaviour) override;
std::shared_ptr<StereoJ2KPictureAssetReader> start_read () const;
diff --git a/test/can_be_read_test.cc b/test/can_be_read_test.cc
new file mode 100644
index 00000000..76615b24
--- /dev/null
+++ b/test/can_be_read_test.cc
@@ -0,0 +1,152 @@
+/*
+ Copyright (C) 2025 Carl Hetherington <cth@carlh.net>
+
+ This file is part of libdcp.
+
+ libdcp is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ libdcp is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with libdcp. If not, see <http://www.gnu.org/licenses/>.
+
+ In addition, as a special exception, the copyright holders give
+ permission to link the code of portions of this program with the
+ OpenSSL library under certain conditions as described in each
+ individual source file, and distribute linked combinations
+ including the two.
+
+ You must obey the GNU General Public License in all respects
+ for all of the code used other than OpenSSL. If you modify
+ file(s) with this exception, you may extend this exception to your
+ version of the file(s), but you are not obligated to do so. If you
+ do not wish to do so, delete this exception statement from your
+ version. If you delete this exception statement from all source
+ files in the program, then also delete it here.
+*/
+
+
+#include "encrypted_kdm.h"
+#include "mono_j2k_picture_asset.h"
+#include "reel_atmos_asset.h"
+#include "reel_mono_picture_asset.h"
+#include "reel_sound_asset.h"
+#include "reel_text_asset.h"
+#include <libcxml/cxml.h>
+#include <boost/test/unit_test.hpp>
+#include "test.h"
+
+
+using std::function;
+using std::shared_ptr;
+
+
+BOOST_AUTO_TEST_CASE(can_be_read_in_reel_mono_picture_asset_unencrypted)
+{
+ cxml::Document doc("Dummy");
+ doc.read_string(
+ "<Dummy>"
+ "<MainPicture>"
+ "<Id>urn:uuid:5407b210-4441-4e97-8b16-8bdc7c12da54</Id>"
+ "<EditRate>25 1</EditRate>"
+ "<IntrinsicDuration>2508</IntrinsicDuration>"
+ "<EntryPoint>225</EntryPoint>"
+ "<Duration>2283</Duration>"
+ "<Hash>hcE3Lb8IEDIre9qXNEt64Z5RcNw=</Hash>"
+ "<FrameRate>25 1</FrameRate>"
+ "<ScreenAspectRatio>1998 1080</ScreenAspectRatio>"
+ "</MainPicture>"
+ "</Dummy>"
+ );
+
+ dcp::ReelMonoPictureAsset reel_asset(doc.node_child("MainPicture"));
+
+ /* Unresolved */
+ BOOST_CHECK(!reel_asset.can_be_read());
+
+ auto asset = std::make_shared<dcp::MonoJ2KPictureAsset>("test/data/DCP/video.mxf");
+ reel_asset.asset_ref().resolve({asset});
+
+ /* Resolved */
+ BOOST_CHECK(reel_asset.can_be_read());
+}
+
+
+BOOST_AUTO_TEST_CASE(can_be_read_in_reel_sound_asset_unencrypted)
+{
+ cxml::Document doc("Dummy");
+ doc.read_string(
+ "<Dummy>"
+ "<MainSound>"
+ "<Id>urn:uuid:97f0f352-5b77-48ee-a558-9df37717f4fa</Id>"
+ "<EditRate>25 1</EditRate>"
+ "<IntrinsicDuration>2508</IntrinsicDuration>"
+ "<EntryPoint>225</EntryPoint>"
+ "<Duration>2283</Duration>"
+ "<Hash>hcE3Lb8IEDIre9qXNEt64Z5RcNw=</Hash>"
+ "</MainSound>"
+ "</Dummy>"
+ );
+
+ dcp::ReelSoundAsset reel_asset(doc.node_child("MainSound"));
+
+ /* Unresolved */
+ BOOST_CHECK(!reel_asset.can_be_read());
+
+ auto asset = std::make_shared<dcp::SoundAsset>("test/data/DCP/audio.mxf");
+ reel_asset.asset_ref().resolve({asset});
+
+ /* Resolved */
+ BOOST_CHECK(reel_asset.can_be_read());
+}
+
+
+static void
+can_be_read_in_reel_encrypted_one(function<shared_ptr<dcp::ReelFileAsset> (shared_ptr<const dcp::Reel>)> get_asset)
+{
+ auto dcp = dcp::DCP(private_test / "data" / "encrypted_dcp_with_subs_and_atmos");
+ dcp.read();
+
+ BOOST_REQUIRE_EQUAL(dcp.cpls().size(), 1U);
+ BOOST_REQUIRE_EQUAL(dcp.cpls()[0]->reels().size(), 1U);
+ auto reel_asset = get_asset(dcp.cpls()[0]->reels()[0]);
+
+ /* Encrypted */
+ BOOST_CHECK(!reel_asset->can_be_read());
+
+ dcp::DecryptedKDM wrong_kdm(
+ dcp::EncryptedKDM(
+ dcp::file_to_string("test/data/other_kdm.xml")
+ ),
+ dcp::file_to_string("test/data/private.key")
+ );
+ dcp.add(wrong_kdm);
+
+ /* Wrong KDM */
+ BOOST_CHECK(!reel_asset->can_be_read());
+
+ dcp::DecryptedKDM right_kdm(
+ dcp::EncryptedKDM(dcp::file_to_string(private_test / "encrypted_dcp_with_subs_and_atmos.xml")),
+ dcp::file_to_string("test/data/private.key")
+ );
+ dcp.add(right_kdm);
+
+ /* Right KDM */
+ BOOST_CHECK(reel_asset->can_be_read());
+}
+
+
+BOOST_AUTO_TEST_CASE(can_be_read_in_reel_encrypted)
+{
+ can_be_read_in_reel_encrypted_one([](shared_ptr<const dcp::Reel> reel) { return reel->main_picture(); });
+ can_be_read_in_reel_encrypted_one([](shared_ptr<const dcp::Reel> reel) { return reel->main_sound(); });
+ can_be_read_in_reel_encrypted_one([](shared_ptr<const dcp::Reel> reel) { return reel->main_subtitle(); });
+ can_be_read_in_reel_encrypted_one([](shared_ptr<const dcp::Reel> reel) { return reel->atmos(); });
+}
+
diff --git a/test/data/other_kdm.xml b/test/data/other_kdm.xml
new file mode 100644
index 00000000..d7b2a22f
--- /dev/null
+++ b/test/data/other_kdm.xml
@@ -0,0 +1,190 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<DCinemaSecurityMessage xmlns="http://www.smpte-ra.org/schemas/430-3/2006/ETM" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:enc="http://www.w3.org/2001/04/xmlenc#">
+ <AuthenticatedPublic Id="ID_AuthenticatedPublic">
+ <MessageId>urn:uuid:30b00f08-6fcf-414a-8924-831fd4daf05d</MessageId>
+ <MessageType>http://www.smpte-ra.org/430-1/2006/KDM#kdm-key-type</MessageType>
+ <AnnotationText>Test_FTR-1_F-133_EN-XX_MOS_2K_20251217_SMPTE_OV</AnnotationText>
+ <IssueDate>2025-12-17T23:46:43+01:00</IssueDate>
+ <Signer>
+ <ds:X509IssuerName>dnQualifier=63rNM95i2Qy0MmrcDFMXewtrA\+c=,CN=.dcpomatic.smpte-430-2.INTERMEDIATE,OU=dcpomatic.com,O=dcpomatic.com</ds:X509IssuerName>
+ <ds:X509SerialNumber>7</ds:X509SerialNumber>
+ </Signer>
+ <RequiredExtensions>
+ <KDMRequiredExtensions xmlns="http://www.smpte-ra.org/schemas/430-1/2006/KDM">
+ <Recipient>
+ <X509IssuerSerial>
+ <ds:X509IssuerName>dnQualifier=enjbKYhZ9JszBVjy71Lg9QrYmV4=,CN=.smpte-430-2.INTERMEDIATE.NOT_FOR_PRODUCTION,OU=example.org,O=example.org</ds:X509IssuerName>
+ <ds:X509SerialNumber>7</ds:X509SerialNumber>
+ </X509IssuerSerial>
+ <X509SubjectName>dnQualifier=MekIXGBkYdh28siMnnF/Zs2JeK8=,CN=CS.smpte-430-2.LEAF.NOT_FOR_PRODUCTION,OU=example.org,O=example.org</X509SubjectName>
+ </Recipient>
+ <CompositionPlaylistId>urn:uuid:67334526-342c-475d-bb64-d8fd8318f954</CompositionPlaylistId>
+ <ContentTitleText>Test_FTR-1_F-133_EN-XX_MOS_2K_20251217_SMPTE_OV</ContentTitleText>
+ <ContentKeysNotValidBefore>2018-04-17T17:46:18+00:00</ContentKeysNotValidBefore>
+ <ContentKeysNotValidAfter>2028-04-10T17:46:18+00:00</ContentKeysNotValidAfter>
+ <AuthorizedDeviceInfo>
+ <DeviceListIdentifier>urn:uuid:bf5b481b-1516-40f2-ba6c-4eca275083bc</DeviceListIdentifier>
+ <DeviceListDescription>smpte-430-2.LEAF.NOT_FOR_PRODUCTION</DeviceListDescription>
+ <DeviceList>
+ <CertificateThumbprint>2jmj7l5rSw0yVb/vlWAYkK/YBwk=</CertificateThumbprint>
+ </DeviceList>
+ </AuthorizedDeviceInfo>
+ <KeyIdList>
+ <TypedKeyId>
+ <KeyType scope="http://www.smpte-ra.org/430-1/2006/KDM#kdm-key-type">MDIK</KeyType>
+ <KeyId>urn:uuid:8380c321-7412-405b-a725-e9251f2de4a5</KeyId>
+ </TypedKeyId>
+ <TypedKeyId>
+ <KeyType scope="http://www.smpte-ra.org/430-1/2006/KDM#kdm-key-type">MDAK</KeyType>
+ <KeyId>urn:uuid:79d60a9e-382f-4fc0-963f-5d643cd8fa7d</KeyId>
+ </TypedKeyId>
+ </KeyIdList>
+ <ForensicMarkFlagList>
+ <ForensicMarkFlag>http://www.smpte-ra.org/430-1/2006/KDM#mrkflg-picture-disable</ForensicMarkFlag>
+ <ForensicMarkFlag>http://www.smpte-ra.org/430-1/2006/KDM#mrkflg-audio-disable</ForensicMarkFlag>
+ </ForensicMarkFlagList>
+ </KDMRequiredExtensions>
+ </RequiredExtensions>
+ <NonCriticalExtensions/>
+ </AuthenticatedPublic>
+ <AuthenticatedPrivate Id="ID_AuthenticatedPrivate">
+ <enc:EncryptedKey xmlns:enc="http://www.w3.org/2001/04/xmlenc#">
+ <enc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p">
+ <ds:DigestMethod xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
+ </enc:EncryptionMethod>
+ <enc:CipherData>
+ <enc:CipherValue>LJE29EIGAw9gJ8s+jGuBuIn6Sh/lbVo75bWbiD1ejoIafXHHigRLbjlnIaNtJZeY
+SqpAWjDk5hCq0wouwu16xub1dCkUCHiKyWpnuaHG+hpInyn+2kAHdrwTp1yIZ1yO
+wnIBwljlnyuqjrFs/CZCpc+LfBxDdRwKkY287XlikGtsFlt2zf97ETAfJBtttumE
+biZe7+DW+He1sgOdu8Oljcxn1NqOpzn4cIeZMQ/JSoucmuVFpB1PvXG9qlQAoeSH
+iBfQXopF1HQsAuAQIvuDdtSISzAslLTu3epg95hR60QYyOjL0zljpkl9pK+Y8ubJ
+cfrGVP7OBgLv2Wxop02CYA==</enc:CipherValue>
+ </enc:CipherData>
+ </enc:EncryptedKey>
+ <enc:EncryptedKey xmlns:enc="http://www.w3.org/2001/04/xmlenc#">
+ <enc:EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p">
+ <ds:DigestMethod xmlns:ds="http://www.w3.org/2000/09/xmldsig#" Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/>
+ </enc:EncryptionMethod>
+ <enc:CipherData>
+ <enc:CipherValue>H7zg5PJxKCINheJo9uEdxS7FIayhO0OsKmOgcud9kEFWT39Z3l/IPsdEdenLH+4E
+dU6wo8UmW2MsC+o9DvNVVedUPialaE2GxUEifXstB47U513tQth00OCogWXFmsv4
+KlMGk9pvhlUBz9XcykXfWYuFa7uYDGrtuFWvWZbqLAQ4vAnl16Gms2NzCDZgbVzI
+6dhI5U6maGWPRl3KFKiTiPahOHciI56B+lgxDUzGfOThzPrYGYHRBpYOt7f4hPIu
+N8UuSHxsT2fZGjWeYjOX0IRU0zeUkt7oZmzJ5ZfYByp+YQGjIFzczmuDca44/DtV
+2QZhSQ4EpYRPPPzaS32hOw==</enc:CipherValue>
+ </enc:CipherData>
+ </enc:EncryptedKey>
+ </AuthenticatedPrivate>
+ <ds:Signature>
+ <ds:SignedInfo>
+ <ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"/>
+ <ds:SignatureMethod Algorithm="http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"/>
+ <ds:Reference URI="#ID_AuthenticatedPublic">
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+ <ds:DigestValue>HaW+lb5HQwllprsijEzwKyBd/XSI5Bnbh20hpu9syC8=</ds:DigestValue>
+ </ds:Reference>
+ <ds:Reference URI="#ID_AuthenticatedPrivate">
+ <ds:DigestMethod Algorithm="http://www.w3.org/2001/04/xmlenc#sha256"/>
+ <ds:DigestValue>1IDrybztmDAN1aweduNBecVn2sohLHdvrXnlbyG/AkY=</ds:DigestValue>
+ </ds:Reference>
+ </ds:SignedInfo>
+ <ds:SignatureValue>UvRcABlbB+dyRFiaiYl3p6bbU2DOD+brzKeX2ky1eRcEyYQhhDbOD5GlHj+yyjXm
+LZXaOY392T9QIS0Qq92KxD4XWmFnzeX+q/KdaQtFYzHtQxINRjhfD6CdXLIBg/O3
+zc+s0jUaneLAo3ZEI1v+dxinglLAB1CVdZMhOOYNK1Kk3oE9X+YQFSumhUmBGHnA
+IdGAQnVEem+qMoFUuEt+emtqnvzdwuv2Xw2xB/f4LqMA3nBRt/Zkn6sqKZ8QwphM
+nnCQlY35/CJdzX7BcCi/agU1hai9Q4UHmDcwwk4+PYL0BQYFQjTssv9R2fht2EBn
+zT4uAQ37egPSTEulTc0llw==</ds:SignatureValue>
+ <ds:KeyInfo>
+ <ds:X509Data>
+ <ds:X509IssuerSerial>
+ <ds:X509IssuerName>dnQualifier=63rNM95i2Qy0MmrcDFMXewtrA\+c=,CN=.dcpomatic.smpte-430-2.INTERMEDIATE,OU=dcpomatic.com,O=dcpomatic.com</ds:X509IssuerName>
+ <ds:X509SerialNumber>7</ds:X509SerialNumber>
+ </ds:X509IssuerSerial>
+ <ds:X509Certificate>MIIEaTCCA1GgAwIBAgIBBzANBgkqhkiG9w0BAQsFADCBhTEWMBQGA1UEChMNZGNw
+b21hdGljLmNvbTEWMBQGA1UECxMNZGNwb21hdGljLmNvbTEsMCoGA1UEAxMjLmRj
+cG9tYXRpYy5zbXB0ZS00MzAtMi5JTlRFUk1FRElBVEUxJTAjBgNVBC4THDYzck5N
+OTVpMlF5ME1tcmNERk1YZXd0ckErYz0wHhcNMTgwNDE2MTc0NjE4WhcNMjgwNDEx
+MTc0NjE4WjB/MRYwFAYDVQQKEw1kY3BvbWF0aWMuY29tMRYwFAYDVQQLEw1kY3Bv
+bWF0aWMuY29tMSYwJAYDVQQDEx1DUy5kY3BvbWF0aWMuc21wdGUtNDMwLTIuTEVB
+RjElMCMGA1UELhMcWHVpL25wcjE3NlJxaENXTmhINUsra3dEMDZVPTCCASIwDQYJ
+KoZIhvcNAQEBBQADggEPADCCAQoCggEBAML7KTWdJSRq9HDqGoN2fUY6c8fO0wvW
+inwR8qcQE2qZXt2swwRJD6pfIzZLHrD/WxNfOne8/+bU1S8OxECZaRDkkW9kReP8
+vSvksFJsAnhenP7KT4VaoCX6D2CTHQYvLGwdQcg0UmRKFbmPxiMcyxO6UCAyYFw+
+kRoeqk7pHwSxhqLCwPiOwiy+3Cv8cygNo3i+yeZTAi0YGOeMD8aEBRfohIlmdOeP
+d38pLAJhERH2aOXqTVEEXE6w3d+JzitNpicwi8miSD71wxKi0XNrH47HFhCgE5Wj
+KyoFXzAHVZTujTc1MpzBaGoEFiBIVOkp1KDEmnxtOC2NaOjx37/BDrECAwEAAaOB
+6DCB5TAMBgNVHRMBAf8EAjAAMAsGA1UdDwQEAwIFoDAdBgNVHQ4EFgQUXui/npr1
+76RqhCWNhH5K+kwD06UwgagGA1UdIwSBoDCBnYAU63rNM95i2Qy0MmrcDFMXewtr
+A+ehgYGkfzB9MRYwFAYDVQQKEw1kY3BvbWF0aWMuY29tMRYwFAYDVQQLEw1kY3Bv
+bWF0aWMuY29tMSQwIgYDVQQDExsuZGNwb21hdGljLnNtcHRlLTQzMC0yLlJPT1Qx
+JTAjBgNVBC4THEJKSzZSMjcrWkYzNTR1NFM0ZitCMFpQclBFTT2CAQYwDQYJKoZI
+hvcNAQELBQADggEBAC9NX+aZ5JfOooUDyEpCTdyk7KJsHt7Ye7bxt2IXXgerfPfO
+zoBua54GORUtjcWpvXMqzhbfD1mAJu2FwhtbF2MQF2d1sowwWhMa9cnwfqsmDylz
+yMFk+meHXTjfZ7AeC6IGnUJOXM1wPCaG3A39rQhP8mWbk1jloF221Gx4Fd4s53t4
+h/zv0ObkP9XiDUhsvqu1/oyysuyLscCXQZL1aWLsKxSfOqunaZjP16MFUyst0GJV
+JuoaqQ260nM/wQO7ieE+c80eGu28ov5W3kjtqowjvM8UB4ep3NrJxZ1y/Xypjnyh
+WsbVStLDVPHxlorfibB4EzSEKyIDBVVPhXs/hGE=</ds:X509Certificate>
+ </ds:X509Data>
+ <ds:X509Data>
+ <ds:X509IssuerSerial>
+ <ds:X509IssuerName>dnQualifier=BJK6R27\+ZF354u4S4f\+B0ZPrPEM=,CN=.dcpomatic.smpte-430-2.ROOT,OU=dcpomatic.com,O=dcpomatic.com</ds:X509IssuerName>
+ <ds:X509SerialNumber>6</ds:X509SerialNumber>
+ </ds:X509IssuerSerial>
+ <ds:X509Certificate>MIIEbTCCA1WgAwIBAgIBBjANBgkqhkiG9w0BAQsFADB9MRYwFAYDVQQKEw1kY3Bv
+bWF0aWMuY29tMRYwFAYDVQQLEw1kY3BvbWF0aWMuY29tMSQwIgYDVQQDExsuZGNw
+b21hdGljLnNtcHRlLTQzMC0yLlJPT1QxJTAjBgNVBC4THEJKSzZSMjcrWkYzNTR1
+NFM0ZitCMFpQclBFTT0wHhcNMTgwNDE2MTc0NjE3WhcNMjgwNDEyMTc0NjE3WjCB
+hTEWMBQGA1UEChMNZGNwb21hdGljLmNvbTEWMBQGA1UECxMNZGNwb21hdGljLmNv
+bTEsMCoGA1UEAxMjLmRjcG9tYXRpYy5zbXB0ZS00MzAtMi5JTlRFUk1FRElBVEUx
+JTAjBgNVBC4THDYzck5NOTVpMlF5ME1tcmNERk1YZXd0ckErYz0wggEiMA0GCSqG
+SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCwPTjVL6DwnBgMdGYgIT5nwRA/TtOAOeU5
+Irqqgjj8wKRLa1H5jBxtEHMLwf2jMiR2aXAiorWLUvdiGhC23KV4bTOy4jGgjNNV
+3Ij6TnKKqhF7NTfLshh7WO4wQqgChDV5smu2wJL2vHYEhmlJp5BUibyyNJiJc7Vw
+223cqJRvIHXFYeclOxlZQnGuU4gDwHZk7hyYxCwdvyr8qt/VBb7oaCvVNw2r/pPB
+DDDzWoANOIHaLufaidauCIm8+pH4TER2qNguRvZi8PwSNYpgiePWvpWDTiUUsOge
+yknoceg9rAdGNQujUgqJPeKmoxGbTlcw5TdzkCfqBmAGFgcjZrGBAgMBAAGjge4w
+geswEgYDVR0TAQH/BAgwBgEB/wIBAjALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFOt6
+zTPeYtkMtDJq3AxTF3sLawPnMIGoBgNVHSMEgaAwgZ2AFASSukdu/mRd+eLuEuH/
+gdGT6zxDoYGBpH8wfTEWMBQGA1UEChMNZGNwb21hdGljLmNvbTEWMBQGA1UECxMN
+ZGNwb21hdGljLmNvbTEkMCIGA1UEAxMbLmRjcG9tYXRpYy5zbXB0ZS00MzAtMi5S
+T09UMSUwIwYDVQQuExxCSks2UjI3K1pGMzU0dTRTNGYrQjBaUHJQRU09ggEFMA0G
+CSqGSIb3DQEBCwUAA4IBAQCjMLdot0fGH4n8ZPbCJX5OLXWm6EXdClsjZlngMTAx
+ENB2nvEOxkBte7C400mLFDlFhnGhWJ8boSrN7sEYuFitXA/X5tI50XfHWzl9o/+2
+VWUsEa1D4e+kCmA8kjwf/lzlMnmsdhlPUdsEFEWNbyh4qAC5smhuXKsZxbqZEizA
+NK1oJ+Np0Hv4g7tX69uQvw5iWQMGYV/RCxh915ILHWeoKG22Tlcf7AxU5VhkOw5F
+Muqu5wgZXbpQ2Mj1ajeL/kwGGI1YbCWm/zHc8jw/LFRWEnCo1wBSkXBHToQVoJF1
+GTtWUcT84jioqTojoFPp1jiOw8oQu8KAFNV9cBH8xShW</ds:X509Certificate>
+ </ds:X509Data>
+ <ds:X509Data>
+ <ds:X509IssuerSerial>
+ <ds:X509IssuerName>dnQualifier=BJK6R27\+ZF354u4S4f\+B0ZPrPEM=,CN=.dcpomatic.smpte-430-2.ROOT,OU=dcpomatic.com,O=dcpomatic.com</ds:X509IssuerName>
+ <ds:X509SerialNumber>5</ds:X509SerialNumber>
+ </ds:X509IssuerSerial>
+ <ds:X509Certificate>MIIEZDCCA0ygAwIBAgIBBTANBgkqhkiG9w0BAQsFADB9MRYwFAYDVQQKEw1kY3Bv
+bWF0aWMuY29tMRYwFAYDVQQLEw1kY3BvbWF0aWMuY29tMSQwIgYDVQQDExsuZGNw
+b21hdGljLnNtcHRlLTQzMC0yLlJPT1QxJTAjBgNVBC4THEJKSzZSMjcrWkYzNTR1
+NFM0ZitCMFpQclBFTT0wHhcNMTgwNDE2MTc0NjE2WhcNMjgwNDEzMTc0NjE2WjB9
+MRYwFAYDVQQKEw1kY3BvbWF0aWMuY29tMRYwFAYDVQQLEw1kY3BvbWF0aWMuY29t
+MSQwIgYDVQQDExsuZGNwb21hdGljLnNtcHRlLTQzMC0yLlJPT1QxJTAjBgNVBC4T
+HEJKSzZSMjcrWkYzNTR1NFM0ZitCMFpQclBFTT0wggEiMA0GCSqGSIb3DQEBAQUA
+A4IBDwAwggEKAoIBAQDeT0ysrx62MNINCICJeUY6GxAJrScKNE0qa3ahRKeslA6n
+K4TIpS2uciAFrmvcqpge5HgoMaR3R+mmJGElWruz4gTh/fYU/Lva2EGmwH4UA803
+u0W1HoWqeut11oAUAx3dCmJFORtlOwj+S7sHlyPP3UJLWjIFnemOK3DORNnjlUw1
+sLAaXoi56xHpJysnNzVhXQeVvzT/7njELsxI85UfSZEpUJ3rSQPRl4nrVep5qGug
+pFO6prbA8w+SIJ9trTBlmSG8In2QlajQnFsO2Y9wYj8kaLewQco1Prb1qODt/h3m
+aeh/+IHjsE494s//zzBNWXbtB5pdKcuVcjnqwELVAgMBAAGjge4wgeswEgYDVR0T
+AQH/BAgwBgEB/wIBAzALBgNVHQ8EBAMCAQYwHQYDVR0OBBYEFASSukdu/mRd+eLu
+EuH/gdGT6zxDMIGoBgNVHSMEgaAwgZ2AFASSukdu/mRd+eLuEuH/gdGT6zxDoYGB
+pH8wfTEWMBQGA1UEChMNZGNwb21hdGljLmNvbTEWMBQGA1UECxMNZGNwb21hdGlj
+LmNvbTEkMCIGA1UEAxMbLmRjcG9tYXRpYy5zbXB0ZS00MzAtMi5ST09UMSUwIwYD
+VQQuExxCSks2UjI3K1pGMzU0dTRTNGYrQjBaUHJQRU09ggEFMA0GCSqGSIb3DQEB
+CwUAA4IBAQCgHdUDSWR+8yN+GDKUpXdrhFylFsiP/QWoOS5qSM7QTP7pihEcB2QF
+Ay4Z9mn1ZIcUFUund4EKaJTRXNZ8341Tm7wyHbrHTy9rb9c/VFIXDnOJMBi8Ac9O
+EM1Z0ZwdBAhmtbm9zUddeHe/lZlPLKTHd+NZ7Pa1hxaamIeCfAiWS5Cmmd7CAOmS
+xnaGf0k/0tCDEp86rWdVW1q0JT/0fbh1IO/rtNrHP1Kz86A5Bc4bYsdMTiqkjcFR
+KwDhgmLrsGYibIgZ0kmcIaP2pd06zRqWa+fgzH+KQI0Lvk7hjqX5qKWxwpLjES1Q
+Kp+MTXgFCZK3CSvMvnmTL9qC5zd3SulH</ds:X509Certificate>
+ </ds:X509Data>
+ </ds:KeyInfo>
+ </ds:Signature>
+</DCinemaSecurityMessage>
diff --git a/test/wscript b/test/wscript
index b4f526bf..3eaef516 100644
--- a/test/wscript
+++ b/test/wscript
@@ -69,6 +69,7 @@ def build(bld):
obj.source = """
asset_test.cc
atmos_test.cc
+ can_be_read_test.cc
certificates_test.cc
colour_test.cc
colour_conversion_test.cc