Verify Id in ContentVersion.
[libdcp.git] / src / verify.cc
index ab6ba115d1f1b36e7247e27c81400ef3988039c6..6611b7b3104899d75ba8fb9d54883bfdac9e9e78 100644 (file)
@@ -1,5 +1,5 @@
 /*
-    Copyright (C) 2018 Carl Hetherington <cth@carlh.net>
+    Copyright (C) 2018-2019 Carl Hetherington <cth@carlh.net>
 
     This file is part of libdcp.
 
 #include "reel_picture_asset.h"
 #include "reel_sound_asset.h"
 #include "exceptions.h"
+#include "compose.hpp"
+#include "raw_convert.h"
 #include <boost/foreach.hpp>
+#include <boost/algorithm/string.hpp>
+#include <boost/regex.hpp>
 #include <list>
 #include <vector>
 #include <iostream>
@@ -53,13 +57,86 @@ using boost::function;
 
 using namespace dcp;
 
-static bool
-verify_asset (shared_ptr<ReelAsset> asset, function<void (float)> progress)
+enum Result {
+       RESULT_GOOD,
+       RESULT_CPL_PKL_DIFFER,
+       RESULT_BAD
+};
+
+static Result
+verify_asset (shared_ptr<DCP> dcp, shared_ptr<ReelMXF> reel_mxf, function<void (float)> progress)
 {
-       string actual_hash = asset->asset_ref()->hash(progress);
-       optional<string> cpl_hash = asset->hash();
-       DCP_ASSERT (cpl_hash);
-       return actual_hash != *cpl_hash;
+       string const actual_hash = reel_mxf->asset_ref()->hash(progress);
+
+       list<shared_ptr<PKL> > pkls = dcp->pkls();
+       /* We've read this DCP in so it must have at least one PKL */
+       DCP_ASSERT (!pkls.empty());
+
+       shared_ptr<Asset> asset = reel_mxf->asset_ref().asset();
+
+       optional<string> pkl_hash;
+       BOOST_FOREACH (shared_ptr<PKL> i, pkls) {
+               pkl_hash = i->hash (reel_mxf->asset_ref()->id());
+               if (pkl_hash) {
+                       break;
+               }
+       }
+
+       DCP_ASSERT (pkl_hash);
+
+       optional<string> cpl_hash = reel_mxf->hash();
+       if (cpl_hash && *cpl_hash != *pkl_hash) {
+               return RESULT_CPL_PKL_DIFFER;
+       }
+
+       if (actual_hash != *pkl_hash) {
+               return RESULT_BAD;
+       }
+
+       return RESULT_GOOD;
+}
+
+static
+bool
+good_urn_uuid (string id)
+{
+       boost::regex ex("urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}");
+       return boost::regex_match (id, ex);
+}
+
+static
+bool
+good_date (string date)
+{
+       boost::regex ex("\\d{4}-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})[+-](\\d{2}):(\\d{2})");
+       boost::match_results<string::const_iterator> res;
+       if (!regex_match (date, res, ex, boost::match_default)) {
+               return false;
+       }
+       int const month = dcp::raw_convert<int>(res[1].str());
+       if (month < 1 || month > 12) {
+               return false;
+       }
+       int const day = dcp::raw_convert<int>(res[2].str());
+       if (day < 1 || day > 31) {
+               return false;
+       }
+       if (dcp::raw_convert<int>(res[3].str()) > 23) {
+               return false;
+       }
+       if (dcp::raw_convert<int>(res[4].str()) > 59) {
+               return false;
+       }
+       if (dcp::raw_convert<int>(res[5].str()) > 59) {
+               return false;
+       }
+       if (dcp::raw_convert<int>(res[6].str()) > 23) {
+               return false;
+       }
+       if (dcp::raw_convert<int>(res[7].str()) > 59) {
+               return false;
+       }
+       return true;
 }
 
 list<VerificationNote>
@@ -74,29 +151,88 @@ dcp::verify (vector<boost::filesystem::path> directories, function<void (string,
 
        BOOST_FOREACH (shared_ptr<DCP> dcp, dcps) {
                stage ("Checking DCP", dcp->directory());
-               DCP::ReadErrors errors;
                try {
-                       dcp->read (true, &errors);
+                       dcp->read (&notes);
                } catch (DCPReadError& e) {
-                       notes.push_back (VerificationNote (VerificationNote::VERIFY_ERROR, e.what ()));
+                       notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::GENERAL_READ, string(e.what())));
                } catch (XMLError& e) {
-                       notes.push_back (VerificationNote (VerificationNote::VERIFY_ERROR, e.what ()));
+                       notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::GENERAL_READ, string(e.what())));
                }
 
                BOOST_FOREACH (shared_ptr<CPL> cpl, dcp->cpls()) {
                        stage ("Checking CPL", cpl->file());
+
+                       cxml::Document cpl_doc ("CompositionPlaylist");
+                       cpl_doc.read_file (cpl->file().get());
+                       if (!good_urn_uuid(cpl_doc.string_child("Id"))) {
+                               notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::BAD_URN_UUID, string("CPL <Id> is malformed")));
+                       }
+                       if (!good_date(cpl_doc.string_child("IssueDate"))) {
+                               notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::BAD_DATE, string("CPL <IssueDate> is malformed")));
+                       }
+                       if (cpl->standard() && cpl->standard().get() == SMPTE && !good_urn_uuid(cpl_doc.node_child("ContentVersion")->string_child("Id"))) {
+                               notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::BAD_URN_UUID, string("<ContentVersion> <Id> is malformed.")));
+                       }
+
+                       /* Check that the CPL's hash corresponds to the PKL */
+                       BOOST_FOREACH (shared_ptr<PKL> i, dcp->pkls()) {
+                               optional<string> h = i->hash(cpl->id());
+                               if (h && make_digest(Data(*cpl->file())) != *h) {
+                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::CPL_HASH_INCORRECT));
+                               }
+                       }
+
                        BOOST_FOREACH (shared_ptr<Reel> reel, cpl->reels()) {
                                stage ("Checking reel", optional<boost::filesystem::path>());
                                if (reel->main_picture()) {
-                                       stage ("Checking picture asset hash", reel->main_picture()->asset()->file());
-                                       if (verify_asset (reel->main_picture(), progress)) {
-                                               notes.push_back (VerificationNote (VerificationNote::VERIFY_ERROR, "Picture asset hash is incorrect"));
+                                       /* Check reel stuff */
+                                       Fraction const frame_rate = reel->main_picture()->frame_rate();
+                                       if (frame_rate.denominator != 1 ||
+                                           (frame_rate.numerator != 24 &&
+                                            frame_rate.numerator != 25 &&
+                                            frame_rate.numerator != 30 &&
+                                            frame_rate.numerator != 48 &&
+                                            frame_rate.numerator != 50 &&
+                                            frame_rate.numerator != 60 &&
+                                            frame_rate.numerator != 96)) {
+                                               notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INVALID_PICTURE_FRAME_RATE));
+                                       }
+                                       /* Check asset */
+                                       if (reel->main_picture()->asset_ref().resolved()) {
+                                               stage ("Checking picture asset hash", reel->main_picture()->asset()->file());
+                                               Result const r = verify_asset (dcp, reel->main_picture(), progress);
+                                               switch (r) {
+                                               case RESULT_BAD:
+                                                       notes.push_back (
+                                                                       VerificationNote(
+                                                                               VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_HASH_INCORRECT, *reel->main_picture()->asset()->file()
+                                                                               )
+                                                                       );
+                                                       break;
+                                               case RESULT_CPL_PKL_DIFFER:
+                                                       notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE));
+                                                       break;
+                                               default:
+                                                       break;
+                                               }
                                        }
                                }
-                               if (reel->main_sound()) {
+                               if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
                                        stage ("Checking sound asset hash", reel->main_sound()->asset()->file());
-                                       if (verify_asset (reel->main_sound(), progress)) {
-                                               notes.push_back (VerificationNote (VerificationNote::VERIFY_ERROR, "Sound asset hash is incorrect"));
+                                       Result const r = verify_asset (dcp, reel->main_sound(), progress);
+                                       switch (r) {
+                                       case RESULT_BAD:
+                                               notes.push_back (
+                                                               VerificationNote(
+                                                                       VerificationNote::VERIFY_ERROR, VerificationNote::SOUND_HASH_INCORRECT, *reel->main_sound()->asset()->file()
+                                                                       )
+                                                               );
+                                               break;
+                                       case RESULT_CPL_PKL_DIFFER:
+                                               notes.push_back (VerificationNote (VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE));
+                                               break;
+                                       default:
+                                               break;
                                        }
                                }
                        }
@@ -105,3 +241,37 @@ dcp::verify (vector<boost::filesystem::path> directories, function<void (string,
 
        return notes;
 }
+
+string
+dcp::note_to_string (dcp::VerificationNote note)
+{
+       switch (note.code()) {
+       case dcp::VerificationNote::GENERAL_READ:
+               return *note.note();
+       case dcp::VerificationNote::CPL_HASH_INCORRECT:
+               return "The hash of the CPL in the PKL does not agree with the CPL file";
+       case dcp::VerificationNote::INVALID_PICTURE_FRAME_RATE:
+               return "The picture in a reel has an invalid frame rate";
+       case dcp::VerificationNote::PICTURE_HASH_INCORRECT:
+               return dcp::String::compose("The hash of the picture asset %1 does not agree with the PKL file", note.file()->filename());
+       case dcp::VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE:
+               return "The PKL and CPL hashes disagree for a picture asset.";
+       case dcp::VerificationNote::SOUND_HASH_INCORRECT:
+               return dcp::String::compose("The hash of the sound asset %1 does not agree with the PKL file", note.file()->filename());
+       case dcp::VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE:
+               return "The PKL and CPL hashes disagree for a sound asset.";
+       case dcp::VerificationNote::EMPTY_ASSET_PATH:
+               return "The asset map contains an empty asset path.";
+       case dcp::VerificationNote::MISSING_ASSET:
+               return "The file for an asset in the asset map cannot be found.";
+       case dcp::VerificationNote::MISMATCHED_STANDARD:
+               return "The DCP contains both SMPTE and Interop parts.";
+       case dcp::VerificationNote::BAD_URN_UUID:
+               return "There is a badly-formed urn:uuid.";
+       case dcp::VerificationNote::BAD_DATE:
+               return "There is a badly-formed date.";
+       }
+
+       return "";
+}
+