Note correct/incorrect hashes when the verifier raises related errors.
authorCarl Hetherington <cth@carlh.net>
Fri, 12 Jan 2024 22:43:23 +0000 (23:43 +0100)
committerCarl Hetherington <cth@carlh.net>
Fri, 12 Jan 2024 22:43:23 +0000 (23:43 +0100)
src/verify.cc
src/verify.h
test/verify_test.cc

index 58eaa9afc743f3c9713eb6c138ed52a14ad3e3c3..0ef4ada5c59b9add2a576e9e6596b84d410b188e 100644 (file)
@@ -385,15 +385,24 @@ enum class VerifyAssetResult {
 
 
 static VerifyAssetResult
-verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
+verify_asset(
+       shared_ptr<const DCP> dcp,
+       shared_ptr<const ReelFileAsset> reel_file_asset,
+       function<void (float)> progress,
+       string* reference_hash,
+       string* calculated_hash
+       )
 {
+       DCP_ASSERT(reference_hash);
+       DCP_ASSERT(calculated_hash);
+
        /* When reading the DCP the hash will have been set to the one from the PKL/CPL.
         * We want to calculate the hash of the actual file contents here, so that we
         * can check it.  unset_hash() means that this calculation will happen on the
         * call to hash().
         */
        reel_file_asset->asset_ref()->unset_hash();
-       auto const actual_hash = reel_file_asset->asset_ref()->hash([progress](int64_t done, int64_t total) {
+       *calculated_hash = reel_file_asset->asset_ref()->hash([progress](int64_t done, int64_t total) {
                progress(float(done) / total);
        });
 
@@ -403,22 +412,23 @@ verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_fi
 
        auto asset = reel_file_asset->asset_ref().asset();
 
-       optional<string> pkl_hash;
+       optional<string> maybe_pkl_hash;
        for (auto i: pkls) {
-               pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
-               if (pkl_hash) {
+               maybe_pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
+               if (maybe_pkl_hash) {
                        break;
                }
        }
 
-       DCP_ASSERT (pkl_hash);
+       DCP_ASSERT(maybe_pkl_hash);
+       *reference_hash = *maybe_pkl_hash;
 
        auto cpl_hash = reel_file_asset->hash();
-       if (cpl_hash && *cpl_hash != *pkl_hash) {
+       if (cpl_hash && *cpl_hash != *reference_hash) {
                return VerifyAssetResult::CPL_PKL_DIFFER;
        }
 
-       if (actual_hash != *pkl_hash) {
+       if (*calculated_hash != *reference_hash) {
                return VerifyAssetResult::BAD;
        }
 
@@ -517,12 +527,18 @@ verify_main_picture_asset (
 
        if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
                stage ("Checking picture asset hash", file);
-               auto const r = verify_asset (dcp, reel_asset, progress);
+               string reference_hash;
+               string calculated_hash;
+               auto const r = verify_asset(dcp, reel_asset, progress, &reference_hash, &calculated_hash);
                switch (r) {
                        case VerifyAssetResult::BAD:
-                               notes.push_back ({
-                                       VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
-                               });
+                               notes.push_back(
+                                       dcp::VerificationNote(
+                                               VerificationNote::Type::ERROR,
+                                               VerificationNote::Code::INCORRECT_PICTURE_HASH,
+                                               file
+                                               ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
+                                       );
                                break;
                        case VerifyAssetResult::CPL_PKL_DIFFER:
                                notes.push_back ({
@@ -613,10 +629,18 @@ verify_main_sound_asset (
 
        if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
                stage("Checking sound asset hash", file);
-               auto const r = verify_asset (dcp, reel_asset, progress);
+               string reference_hash;
+               string calculated_hash;
+               auto const r = verify_asset(dcp, reel_asset, progress, &reference_hash, &calculated_hash);
                switch (r) {
                        case VerifyAssetResult::BAD:
-                               notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
+                               notes.push_back(
+                                       dcp::VerificationNote(
+                                               VerificationNote::Type::ERROR,
+                                               VerificationNote::Code::INCORRECT_SOUND_HASH,
+                                               file
+                                               ).set_reference_hash(reference_hash).set_calculated_hash(calculated_hash)
+                                       );
                                break;
                        case VerifyAssetResult::CPL_PKL_DIFFER:
                                notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
@@ -1585,8 +1609,16 @@ verify_cpl(
        for (auto i: dcp->pkls()) {
                /* Check that the CPL's hash corresponds to the PKL */
                optional<string> h = i->hash(cpl->id());
-               if (h && make_digest(ArrayData(*cpl->file())) != *h) {
-                       notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
+               auto calculated_cpl_hash = make_digest(ArrayData(*cpl->file()));
+               if (h && calculated_cpl_hash != *h) {
+                       notes.push_back(
+                               dcp::VerificationNote(
+                                       VerificationNote::Type::ERROR,
+                                       VerificationNote::Code::MISMATCHED_CPL_HASHES,
+                                       cpl->id(),
+                                       cpl->file().get()
+                                       ).set_calculated_hash(calculated_cpl_hash).set_reference_hash(*h)
+                               );
                }
 
                /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
@@ -1918,15 +1950,15 @@ dcp::note_to_string (VerificationNote note)
        case VerificationNote::Code::FAILED_READ:
                return *note.note();
        case VerificationNote::Code::MISMATCHED_CPL_HASHES:
-               return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
+               return String::compose("The hash (%1) of the CPL (%2) in the PKL does not agree with the CPL file (%3).", note.reference_hash().get(), note.note().get(), note.calculated_hash().get());
        case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
                return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
        case VerificationNote::Code::INCORRECT_PICTURE_HASH:
-               return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
+               return String::compose("The hash (%1) of the picture asset %2 does not agree with the PKL file (%3).", note.calculated_hash().get(), note.file()->filename(), note.reference_hash().get());
        case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
                return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
        case VerificationNote::Code::INCORRECT_SOUND_HASH:
-               return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
+               return String::compose("The hash (%1) of the sound asset %2 does not agree with the PKL file (%3).", note.calculated_hash().get(), note.file()->filename(), note.reference_hash().get());
        case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
                return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
        case VerificationNote::Code::EMPTY_ASSET_PATH:
@@ -2161,7 +2193,9 @@ dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
                a.size() == b.size() &&
                a.id() == b.id() &&
                a.other_id() == b.other_id() &&
-               a.frame_rate() == b.frame_rate();
+               a.frame_rate() == b.frame_rate() &&
+               a.reference_hash() == b.reference_hash() &&
+               a.calculated_hash() == b.calculated_hash();
 }
 
 
index 7bfe4217c5f934cc77ac8725003363ea0727bcd4..4ca3297ac582d1b01c9f541d7f812b2d320414df 100644 (file)
@@ -101,6 +101,8 @@ public:
                /** The hash of the CPL in the PKL does not agree with the CPL file
                 *  note contains CPL ID
                 *  file contains CPL filename
+                *  calculated_hash contains current hash of the CPL
+                *  reference_hash contains the hash written in the PKL
                 */
                MISMATCHED_CPL_HASHES,
                /** The frame rate given in a reel for the main picture is not 24, 25, 30, 48, 50 or 60
@@ -109,6 +111,8 @@ public:
                INVALID_PICTURE_FRAME_RATE,
                /** The hash of a main picture asset does not agree with the PKL file
                 *  file contains the picture asset filename
+                *  calculated_hash contains the current hash of the picture MXF
+                *  reference_hash contains the hash from the PKL
                 */
                INCORRECT_PICTURE_HASH,
                /** The hash of a main picture is different in the CPL and PKL
@@ -117,6 +121,8 @@ public:
                MISMATCHED_PICTURE_HASHES,
                /** The hash of a main sound asset does not agree with the PKL file
                 *  file contains the sound asset filename
+                *  calculated_hash contains the current hash of the picture MXF
+                *  reference_hash contains the hash from the PKL
                 */
                INCORRECT_SOUND_HASH,
                /** The hash of a main sound is different in the CPL and PKL
@@ -525,7 +531,9 @@ private:
                SIZE,
                ID,
                OTHER_ID,
-               FRAME_RATE
+               FRAME_RATE,
+               CALCULATED_HASH,
+               REFERENCE_HASH
        };
 
        template <class T>
@@ -605,6 +613,24 @@ public:
                return data<int>(Data::FRAME_RATE);
        }
 
+       VerificationNote& set_calculated_hash(std::string hash) {
+               _data[Data::CALCULATED_HASH] = hash;
+               return *this;
+       }
+
+       boost::optional<std::string> calculated_hash() const {
+               return data<std::string>(Data::CALCULATED_HASH);
+       }
+
+       VerificationNote& set_reference_hash(std::string hash) {
+               _data[Data::REFERENCE_HASH] = hash;
+               return *this;
+       }
+
+       boost::optional<std::string> reference_hash() const {
+               return data<std::string>(Data::REFERENCE_HASH);
+       }
+
 private:
        Type _type;
        Code _code;
index 0e15624d48d836186e906d1bd5cbf132f6716420..e55e0b6e29b8d9079c7cf7bfa8ecdbdf33dac1fe 100644 (file)
@@ -207,24 +207,28 @@ check_verify_result(vector<path> dir, vector<dcp::DecryptedKDM> kdm, vector<dcp:
        for (auto i: notes) {
                message += "  " + note_to_string(i) + "\n";
                message += dcp::String::compose(
-                       "  [%1 %2 %3 %4 %5]\n",
+                       "  [%1 %2 %3 %4 %5 %6 %7]\n",
                        static_cast<int>(i.type()),
                        static_cast<int>(i.code()),
                        i.note().get_value_or("<none>"),
                        i.file().get_value_or("<none>"),
-                       i.line().get_value_or(0)
+                       i.line().get_value_or(0),
+                       i.reference_hash().get_value_or("<none>"),
+                       i.calculated_hash().get_value_or("<none>")
                        );
        }
        message += "Expected:\n";
        for (auto i: test_notes) {
                message += "  " + note_to_string(i) + "\n";
                message += dcp::String::compose(
-                       "  [%1 %2 %3 %4 %5]\n",
+                       "  [%1 %2 %3 %4 %5 %6 %7]\n",
                        static_cast<int>(i.type()),
                        static_cast<int>(i.code()),
                        i.note().get_value_or("<none>"),
                        i.file().get_value_or("<none>"),
-                       i.line().get_value_or(0)
+                       i.line().get_value_or(0),
+                       i.reference_hash().get_value_or("<none>"),
+                       i.calculated_hash().get_value_or("<none>")
                        );
        }
 
@@ -269,6 +273,28 @@ add_font(shared_ptr<dcp::SubtitleAsset> asset)
 }
 
 
+class HashCalculator
+{
+public:
+       HashCalculator(boost::filesystem::path path)
+               : _path(path)
+               , _old_hash(dcp::make_digest(path, [](int64_t, int64_t) {}))
+       {}
+
+       std::string old_hash() const {
+               return _old_hash;
+       }
+
+       std::string new_hash() const {
+               return dcp::make_digest(_path, [](int64_t, int64_t) {});
+       }
+
+private:
+       boost::filesystem::path _path;
+       std::string _old_hash;
+};
+
+
 BOOST_AUTO_TEST_CASE (verify_no_error)
 {
        stages.clear ();
@@ -310,8 +336,7 @@ BOOST_AUTO_TEST_CASE (verify_no_error)
        BOOST_CHECK_EQUAL (st->first, "Checking PKL");
        BOOST_REQUIRE (st->second);
        BOOST_CHECK_EQUAL (st->second.get(), canonical(pkl_file));
-       ++st;
-       BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
+       ++st; BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
        BOOST_REQUIRE (st->second);
        BOOST_CHECK_EQUAL (st->second.get(), canonical(assetmap_file));
        ++st;
@@ -328,6 +353,7 @@ BOOST_AUTO_TEST_CASE (verify_incorrect_picture_sound_hash)
        auto dir = setup (1, "incorrect_picture_sound_hash");
 
        auto video_path = path(dir / "video.mxf");
+       HashCalculator video_calc(video_path);
        auto mod = fopen(video_path.string().c_str(), "r+b");
        BOOST_REQUIRE (mod);
        fseek (mod, 4096, SEEK_SET);
@@ -336,6 +362,7 @@ BOOST_AUTO_TEST_CASE (verify_incorrect_picture_sound_hash)
        fclose (mod);
 
        auto audio_path = path(dir / "audio.mxf");
+       HashCalculator audio_calc(audio_path);
        mod = fopen(audio_path.string().c_str(), "r+b");
        BOOST_REQUIRE (mod);
        BOOST_REQUIRE_EQUAL (fseek(mod, -64, SEEK_END), 0);
@@ -347,8 +374,12 @@ BOOST_AUTO_TEST_CASE (verify_incorrect_picture_sound_hash)
                { dir },
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_PICTURE_HASH, canonical(video_path) },
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_SOUND_HASH, canonical(audio_path) },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_PICTURE_HASH, canonical(video_path)
+                               ).set_reference_hash(video_calc.old_hash()).set_calculated_hash(video_calc.new_hash()),
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_SOUND_HASH, canonical(audio_path)
+                               ).set_reference_hash(audio_calc.old_hash()).set_calculated_hash(audio_calc.new_hash()),
                });
 }
 
@@ -359,6 +390,8 @@ BOOST_AUTO_TEST_CASE (verify_mismatched_picture_sound_hashes)
 
        auto dir = setup (1, "mismatched_picture_sound_hashes");
 
+       HashCalculator calc(dir / dcp_test1_cpl());
+
        {
                Editor e (dir / dcp_test1_pkl());
                e.replace ("<Hash>", "<Hash>x");
@@ -368,7 +401,9 @@ BOOST_AUTO_TEST_CASE (verify_mismatched_picture_sound_hashes)
                { dir },
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id(), canonical(dir / dcp_test1_cpl()) },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id(), canonical(dir / dcp_test1_cpl())
+                               ).set_reference_hash("x" + calc.old_hash()).set_calculated_hash(calc.old_hash()),
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_PICTURE_HASHES, canonical(dir / "video.mxf") },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_SOUND_HASHES, canonical(dir / "audio.mxf") },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xKcJb7S2K5cNm8RG4kfQD5FTeS0A=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl()), 28 },
@@ -382,6 +417,8 @@ BOOST_AUTO_TEST_CASE (verify_failed_read_content_kind)
 {
        auto dir = setup (1, "failed_read_content_kind");
 
+       HashCalculator calc(dir / dcp_test1_cpl());
+
        {
                Editor e (dir / dcp_test1_cpl());
                e.replace ("<ContentKind>", "<ContentKind>x");
@@ -391,7 +428,9 @@ BOOST_AUTO_TEST_CASE (verify_failed_read_content_kind)
                { dir },
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id(), canonical(dir / dcp_test1_cpl()) },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id(), canonical(dir / dcp_test1_cpl())
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash("4v/mVjs1Rw0NELxgyHa5rSpoBPA="),
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_CONTENT_KIND, string("xtrailer") }
                });
 }
@@ -1065,6 +1104,8 @@ BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_bad_tag)
        dcp.set_annotation_text("hello");
        dcp.write_xml();
 
+       HashCalculator calc(find_cpl(dir));
+
        {
                Editor e (find_cpl(dir));
                e.replace ("MainSound", "MainSoundX");
@@ -1088,7 +1129,9 @@ BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_bad_tag)
                                canonical(cpl->file().get()),
                                71
                        },
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get())
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash())
                });
 }
 
@@ -2287,6 +2330,8 @@ BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
 
        auto const cpl = dcp->cpls()[0];
 
+       HashCalculator calc(cpl->file().get());
+
        {
                BOOST_REQUIRE (cpl->file());
                Editor e(cpl->file().get());
@@ -2298,7 +2343,9 @@ BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
                {},
                {
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get())
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash())
                });
 }
 
@@ -2312,6 +2359,8 @@ BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
        BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
        auto const cpl = dcp->cpls()[0];
 
+       HashCalculator calc(cpl->file().get());
+
        {
                BOOST_REQUIRE (cpl->file());
                Editor e(cpl->file().get());
@@ -2323,7 +2372,9 @@ BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
                {},
                {
                        { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get())
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash())
                });
 }
 
@@ -2623,6 +2674,8 @@ BOOST_AUTO_TEST_CASE (verify_missing_hash)
        BOOST_REQUIRE (cpl->reels()[0]->main_picture());
        auto asset_id = cpl->reels()[0]->main_picture()->id();
 
+       HashCalculator calc(cpl->file().get());
+
        {
                BOOST_REQUIRE (cpl->file());
                Editor e(cpl->file().get());
@@ -2633,7 +2686,9 @@ BOOST_AUTO_TEST_CASE (verify_missing_hash)
                {dir},
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
                });
 }
@@ -2765,6 +2820,8 @@ BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
        BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
        auto cpl = dcp->cpls()[0];
 
+       HashCalculator calc(cpl->file().get());
+
        {
                Editor e (cpl->file().get());
                e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
@@ -2774,7 +2831,9 @@ BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
                {dir},
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
                });
 }
@@ -2788,6 +2847,8 @@ BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
 
        auto cpl = dcp->cpls()[0];
 
+       HashCalculator calc(cpl->file().get());
+
        {
                Editor e (cpl->file().get());
                e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
@@ -2797,7 +2858,9 @@ BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
                {dir},
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
                });
 }
@@ -2811,6 +2874,8 @@ BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
 
        auto const cpl = dcp->cpls()[0];
 
+       HashCalculator calc(cpl->file().get());
+
        {
                Editor e (cpl->file().get());
                e.replace ("<meta:Name>A", "<meta:NameX>A");
@@ -2823,7 +2888,9 @@ BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
                {
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:NameX'"), cpl->file().get(), 70 },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:NameX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 77 },
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                });
 }
 
@@ -2836,6 +2903,8 @@ BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
 
        auto cpl = dcp->cpls()[0];
 
+       HashCalculator calc(cpl->file().get());
+
        {
                Editor e (cpl->file().get());
                e.replace ("Application", "Fred");
@@ -2845,7 +2914,9 @@ BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
                {dir},
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
                });
 }
@@ -2859,6 +2930,8 @@ BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
 
        auto cpl = dcp->cpls()[0];
 
+       HashCalculator calc(cpl->file().get());
+
        {
                Editor e (cpl->file().get());
                e.replace ("DCP Constraints Profile", "Fred");
@@ -2868,7 +2941,9 @@ BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
                {dir},
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
                });
 }
@@ -2882,6 +2957,8 @@ BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
 
        auto const cpl = dcp->cpls()[0];
 
+       HashCalculator calc(cpl->file().get());
+
        {
                Editor e (cpl->file().get());
                e.replace ("<meta:Value>", "<meta:ValueX>");
@@ -2894,7 +2971,9 @@ BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
                {
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 74 },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:ValueX' is not allowed for content model '(Name,Value)'"), cpl->file().get(), 75 },
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                });
 }
 
@@ -2907,6 +2986,8 @@ BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
 
        auto const cpl = dcp->cpls()[0];
 
+       HashCalculator calc(cpl->file().get());
+
        {
                Editor e (cpl->file().get());
                e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
@@ -2916,7 +2997,9 @@ BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
                {dir},
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'"), cpl->file().get() },
                });
 }
@@ -2930,6 +3013,8 @@ BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
 
        auto const cpl = dcp->cpls()[0];
 
+       HashCalculator calc(cpl->file().get());
+
        {
                Editor e (cpl->file().get());
                e.replace ("<meta:Property>", "<meta:PropertyX>");
@@ -2942,7 +3027,9 @@ BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
                {
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 72 },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 76 },
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                });
 }
 
@@ -2955,6 +3042,8 @@ BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
 
        auto const cpl = dcp->cpls()[0];
 
+       HashCalculator calc(cpl->file().get());
+
        {
                Editor e (cpl->file().get());
                e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
@@ -2967,7 +3056,9 @@ BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
                {
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 71 },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyListX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 77 },
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                });
 }
 
@@ -2984,6 +3075,8 @@ BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
        path const pkl = dir / ( "pkl_" + encryption_test_pkl_id() + ".xml" );
        path const cpl = dir / ( "cpl_" + encryption_test_cpl_id() + ".xml");
 
+       HashCalculator calc(cpl);
+
        {
                Editor e (cpl);
                e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
@@ -2993,7 +3086,9 @@ BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
                {dir},
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id(), canonical(cpl) },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id(), canonical(cpl)
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id(), canonical(pkl), },
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
                        { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
@@ -3317,6 +3412,8 @@ BOOST_AUTO_TEST_CASE (verify_unexpected_things_in_main_markers)
        auto dcp = make_simple (dir, 1, 24);
        dcp->write_xml();
 
+       HashCalculator calc(find_cpl(dir));
+
        {
                Editor e (find_cpl(dir));
                e.insert(
@@ -3331,7 +3428,9 @@ BOOST_AUTO_TEST_CASE (verify_unexpected_things_in_main_markers)
                { dir },
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir))
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_ENTRY_POINT },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_DURATION },
                });
@@ -3345,6 +3444,8 @@ BOOST_AUTO_TEST_CASE(verify_invalid_content_kind)
        auto dcp = make_simple (dir, 1, 24);
        dcp->write_xml();
 
+       HashCalculator calc(find_cpl(dir));
+
        {
                Editor e(find_cpl(dir));
                e.replace("trailer", "trip");
@@ -3356,7 +3457,9 @@ BOOST_AUTO_TEST_CASE(verify_invalid_content_kind)
                { dir },
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir))
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_CONTENT_KIND, string("trip") }
                });
 
@@ -3370,6 +3473,8 @@ BOOST_AUTO_TEST_CASE(verify_valid_content_kind)
        auto dcp = make_simple (dir, 1, 24);
        dcp->write_xml();
 
+       HashCalculator calc(find_cpl(dir));
+
        {
                Editor e(find_cpl(dir));
                e.replace("<ContentKind>trailer</ContentKind>", "<ContentKind scope=\"http://bobs.contents/\">trip</ContentKind>");
@@ -3381,7 +3486,9 @@ BOOST_AUTO_TEST_CASE(verify_valid_content_kind)
                { dir },
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir))
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                });
 
 }
@@ -3396,6 +3503,8 @@ BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_1)
 
        auto constexpr area = "<meta:MainPictureActiveArea>";
 
+       HashCalculator calc(find_cpl(dir));
+
        {
                Editor e(find_cpl(dir));
                e.delete_lines_after(area, 2);
@@ -3410,7 +3519,9 @@ BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_1)
                { dir },
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir))
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "width 1997 is not a multiple of 2", canonical(find_cpl(dir)) },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 4080 is bigger than the asset height 1080", canonical(find_cpl(dir)) },
                });
@@ -3426,6 +3537,8 @@ BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_2)
 
        auto constexpr area = "<meta:MainPictureActiveArea>";
 
+       HashCalculator calc(find_cpl(dir));
+
        {
                Editor e(find_cpl(dir));
                e.delete_lines_after(area, 2);
@@ -3440,7 +3553,9 @@ BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_2)
                { dir },
                {},
                {
-                       { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
+                       dcp::VerificationNote(
+                               dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir))
+                               ).set_reference_hash(calc.old_hash()).set_calculated_hash(calc.new_hash()),
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 5125 is not a multiple of 2", canonical(find_cpl(dir)) },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "width 9900 is bigger than the asset width 1998", canonical(find_cpl(dir)) },
                        { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 5125 is bigger than the asset height 1080", canonical(find_cpl(dir)) },