Verify Id in ContentVersion.
[libdcp.git] / src / verify.cc
1 /*
2     Copyright (C) 2018-2019 Carl Hetherington <cth@carlh.net>
3
4     This file is part of libdcp.
5
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.
10
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.
15
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/>.
18
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
23     including the two.
24
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.
32 */
33
34 #include "verify.h"
35 #include "dcp.h"
36 #include "cpl.h"
37 #include "reel.h"
38 #include "reel_picture_asset.h"
39 #include "reel_sound_asset.h"
40 #include "exceptions.h"
41 #include "compose.hpp"
42 #include "raw_convert.h"
43 #include <boost/foreach.hpp>
44 #include <boost/algorithm/string.hpp>
45 #include <boost/regex.hpp>
46 #include <list>
47 #include <vector>
48 #include <iostream>
49
50 using std::list;
51 using std::vector;
52 using std::string;
53 using std::cout;
54 using boost::shared_ptr;
55 using boost::optional;
56 using boost::function;
57
58 using namespace dcp;
59
60 enum Result {
61         RESULT_GOOD,
62         RESULT_CPL_PKL_DIFFER,
63         RESULT_BAD
64 };
65
66 static Result
67 verify_asset (shared_ptr<DCP> dcp, shared_ptr<ReelMXF> reel_mxf, function<void (float)> progress)
68 {
69         string const actual_hash = reel_mxf->asset_ref()->hash(progress);
70
71         list<shared_ptr<PKL> > pkls = dcp->pkls();
72         /* We've read this DCP in so it must have at least one PKL */
73         DCP_ASSERT (!pkls.empty());
74
75         shared_ptr<Asset> asset = reel_mxf->asset_ref().asset();
76
77         optional<string> pkl_hash;
78         BOOST_FOREACH (shared_ptr<PKL> i, pkls) {
79                 pkl_hash = i->hash (reel_mxf->asset_ref()->id());
80                 if (pkl_hash) {
81                         break;
82                 }
83         }
84
85         DCP_ASSERT (pkl_hash);
86
87         optional<string> cpl_hash = reel_mxf->hash();
88         if (cpl_hash && *cpl_hash != *pkl_hash) {
89                 return RESULT_CPL_PKL_DIFFER;
90         }
91
92         if (actual_hash != *pkl_hash) {
93                 return RESULT_BAD;
94         }
95
96         return RESULT_GOOD;
97 }
98
99 static
100 bool
101 good_urn_uuid (string id)
102 {
103         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}");
104         return boost::regex_match (id, ex);
105 }
106
107 static
108 bool
109 good_date (string date)
110 {
111         boost::regex ex("\\d{4}-(\\d{2})-(\\d{2})T(\\d{2}):(\\d{2}):(\\d{2})[+-](\\d{2}):(\\d{2})");
112         boost::match_results<string::const_iterator> res;
113         if (!regex_match (date, res, ex, boost::match_default)) {
114                 return false;
115         }
116         int const month = dcp::raw_convert<int>(res[1].str());
117         if (month < 1 || month > 12) {
118                 return false;
119         }
120         int const day = dcp::raw_convert<int>(res[2].str());
121         if (day < 1 || day > 31) {
122                 return false;
123         }
124         if (dcp::raw_convert<int>(res[3].str()) > 23) {
125                 return false;
126         }
127         if (dcp::raw_convert<int>(res[4].str()) > 59) {
128                 return false;
129         }
130         if (dcp::raw_convert<int>(res[5].str()) > 59) {
131                 return false;
132         }
133         if (dcp::raw_convert<int>(res[6].str()) > 23) {
134                 return false;
135         }
136         if (dcp::raw_convert<int>(res[7].str()) > 59) {
137                 return false;
138         }
139         return true;
140 }
141
142 list<VerificationNote>
143 dcp::verify (vector<boost::filesystem::path> directories, function<void (string, optional<boost::filesystem::path>)> stage, function<void (float)> progress)
144 {
145         list<VerificationNote> notes;
146
147         list<shared_ptr<DCP> > dcps;
148         BOOST_FOREACH (boost::filesystem::path i, directories) {
149                 dcps.push_back (shared_ptr<DCP> (new DCP (i)));
150         }
151
152         BOOST_FOREACH (shared_ptr<DCP> dcp, dcps) {
153                 stage ("Checking DCP", dcp->directory());
154                 try {
155                         dcp->read (&notes);
156                 } catch (DCPReadError& e) {
157                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::GENERAL_READ, string(e.what())));
158                 } catch (XMLError& e) {
159                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::GENERAL_READ, string(e.what())));
160                 }
161
162                 BOOST_FOREACH (shared_ptr<CPL> cpl, dcp->cpls()) {
163                         stage ("Checking CPL", cpl->file());
164
165                         cxml::Document cpl_doc ("CompositionPlaylist");
166                         cpl_doc.read_file (cpl->file().get());
167                         if (!good_urn_uuid(cpl_doc.string_child("Id"))) {
168                                 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::BAD_URN_UUID, string("CPL <Id> is malformed")));
169                         }
170                         if (!good_date(cpl_doc.string_child("IssueDate"))) {
171                                 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::BAD_DATE, string("CPL <IssueDate> is malformed")));
172                         }
173                         if (cpl->standard() && cpl->standard().get() == SMPTE && !good_urn_uuid(cpl_doc.node_child("ContentVersion")->string_child("Id"))) {
174                                 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::Code::BAD_URN_UUID, string("<ContentVersion> <Id> is malformed.")));
175                         }
176
177                         /* Check that the CPL's hash corresponds to the PKL */
178                         BOOST_FOREACH (shared_ptr<PKL> i, dcp->pkls()) {
179                                 optional<string> h = i->hash(cpl->id());
180                                 if (h && make_digest(Data(*cpl->file())) != *h) {
181                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::CPL_HASH_INCORRECT));
182                                 }
183                         }
184
185                         BOOST_FOREACH (shared_ptr<Reel> reel, cpl->reels()) {
186                                 stage ("Checking reel", optional<boost::filesystem::path>());
187                                 if (reel->main_picture()) {
188                                         /* Check reel stuff */
189                                         Fraction const frame_rate = reel->main_picture()->frame_rate();
190                                         if (frame_rate.denominator != 1 ||
191                                             (frame_rate.numerator != 24 &&
192                                              frame_rate.numerator != 25 &&
193                                              frame_rate.numerator != 30 &&
194                                              frame_rate.numerator != 48 &&
195                                              frame_rate.numerator != 50 &&
196                                              frame_rate.numerator != 60 &&
197                                              frame_rate.numerator != 96)) {
198                                                 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INVALID_PICTURE_FRAME_RATE));
199                                         }
200                                         /* Check asset */
201                                         if (reel->main_picture()->asset_ref().resolved()) {
202                                                 stage ("Checking picture asset hash", reel->main_picture()->asset()->file());
203                                                 Result const r = verify_asset (dcp, reel->main_picture(), progress);
204                                                 switch (r) {
205                                                 case RESULT_BAD:
206                                                         notes.push_back (
207                                                                         VerificationNote(
208                                                                                 VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_HASH_INCORRECT, *reel->main_picture()->asset()->file()
209                                                                                 )
210                                                                         );
211                                                         break;
212                                                 case RESULT_CPL_PKL_DIFFER:
213                                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE));
214                                                         break;
215                                                 default:
216                                                         break;
217                                                 }
218                                         }
219                                 }
220                                 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
221                                         stage ("Checking sound asset hash", reel->main_sound()->asset()->file());
222                                         Result const r = verify_asset (dcp, reel->main_sound(), progress);
223                                         switch (r) {
224                                         case RESULT_BAD:
225                                                 notes.push_back (
226                                                                 VerificationNote(
227                                                                         VerificationNote::VERIFY_ERROR, VerificationNote::SOUND_HASH_INCORRECT, *reel->main_sound()->asset()->file()
228                                                                         )
229                                                                 );
230                                                 break;
231                                         case RESULT_CPL_PKL_DIFFER:
232                                                 notes.push_back (VerificationNote (VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE));
233                                                 break;
234                                         default:
235                                                 break;
236                                         }
237                                 }
238                         }
239                 }
240         }
241
242         return notes;
243 }
244
245 string
246 dcp::note_to_string (dcp::VerificationNote note)
247 {
248         switch (note.code()) {
249         case dcp::VerificationNote::GENERAL_READ:
250                 return *note.note();
251         case dcp::VerificationNote::CPL_HASH_INCORRECT:
252                 return "The hash of the CPL in the PKL does not agree with the CPL file";
253         case dcp::VerificationNote::INVALID_PICTURE_FRAME_RATE:
254                 return "The picture in a reel has an invalid frame rate";
255         case dcp::VerificationNote::PICTURE_HASH_INCORRECT:
256                 return dcp::String::compose("The hash of the picture asset %1 does not agree with the PKL file", note.file()->filename());
257         case dcp::VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE:
258                 return "The PKL and CPL hashes disagree for a picture asset.";
259         case dcp::VerificationNote::SOUND_HASH_INCORRECT:
260                 return dcp::String::compose("The hash of the sound asset %1 does not agree with the PKL file", note.file()->filename());
261         case dcp::VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE:
262                 return "The PKL and CPL hashes disagree for a sound asset.";
263         case dcp::VerificationNote::EMPTY_ASSET_PATH:
264                 return "The asset map contains an empty asset path.";
265         case dcp::VerificationNote::MISSING_ASSET:
266                 return "The file for an asset in the asset map cannot be found.";
267         case dcp::VerificationNote::MISMATCHED_STANDARD:
268                 return "The DCP contains both SMPTE and Interop parts.";
269         case dcp::VerificationNote::BAD_URN_UUID:
270                 return "There is a badly-formed urn:uuid.";
271         case dcp::VerificationNote::BAD_DATE:
272                 return "There is a badly-formed date.";
273         }
274
275         return "";
276 }
277