Use regex and check <IssueDate> in CPL.
[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
174                         /* Check that the CPL's hash corresponds to the PKL */
175                         BOOST_FOREACH (shared_ptr<PKL> i, dcp->pkls()) {
176                                 optional<string> h = i->hash(cpl->id());
177                                 if (h && make_digest(Data(*cpl->file())) != *h) {
178                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::CPL_HASH_INCORRECT));
179                                 }
180                         }
181
182                         BOOST_FOREACH (shared_ptr<Reel> reel, cpl->reels()) {
183                                 stage ("Checking reel", optional<boost::filesystem::path>());
184                                 if (reel->main_picture()) {
185                                         /* Check reel stuff */
186                                         Fraction const frame_rate = reel->main_picture()->frame_rate();
187                                         if (frame_rate.denominator != 1 ||
188                                             (frame_rate.numerator != 24 &&
189                                              frame_rate.numerator != 25 &&
190                                              frame_rate.numerator != 30 &&
191                                              frame_rate.numerator != 48 &&
192                                              frame_rate.numerator != 50 &&
193                                              frame_rate.numerator != 60 &&
194                                              frame_rate.numerator != 96)) {
195                                                 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INVALID_PICTURE_FRAME_RATE));
196                                         }
197                                         /* Check asset */
198                                         if (reel->main_picture()->asset_ref().resolved()) {
199                                                 stage ("Checking picture asset hash", reel->main_picture()->asset()->file());
200                                                 Result const r = verify_asset (dcp, reel->main_picture(), progress);
201                                                 switch (r) {
202                                                 case RESULT_BAD:
203                                                         notes.push_back (
204                                                                         VerificationNote(
205                                                                                 VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_HASH_INCORRECT, *reel->main_picture()->asset()->file()
206                                                                                 )
207                                                                         );
208                                                         break;
209                                                 case RESULT_CPL_PKL_DIFFER:
210                                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE));
211                                                         break;
212                                                 default:
213                                                         break;
214                                                 }
215                                         }
216                                 }
217                                 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
218                                         stage ("Checking sound asset hash", reel->main_sound()->asset()->file());
219                                         Result const r = verify_asset (dcp, reel->main_sound(), progress);
220                                         switch (r) {
221                                         case RESULT_BAD:
222                                                 notes.push_back (
223                                                                 VerificationNote(
224                                                                         VerificationNote::VERIFY_ERROR, VerificationNote::SOUND_HASH_INCORRECT, *reel->main_sound()->asset()->file()
225                                                                         )
226                                                                 );
227                                                 break;
228                                         case RESULT_CPL_PKL_DIFFER:
229                                                 notes.push_back (VerificationNote (VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE));
230                                                 break;
231                                         default:
232                                                 break;
233                                         }
234                                 }
235                         }
236                 }
237         }
238
239         return notes;
240 }
241
242 string
243 dcp::note_to_string (dcp::VerificationNote note)
244 {
245         switch (note.code()) {
246         case dcp::VerificationNote::GENERAL_READ:
247                 return *note.note();
248         case dcp::VerificationNote::CPL_HASH_INCORRECT:
249                 return "The hash of the CPL in the PKL does not agree with the CPL file";
250         case dcp::VerificationNote::INVALID_PICTURE_FRAME_RATE:
251                 return "The picture in a reel has an invalid frame rate";
252         case dcp::VerificationNote::PICTURE_HASH_INCORRECT:
253                 return dcp::String::compose("The hash of the picture asset %1 does not agree with the PKL file", note.file()->filename());
254         case dcp::VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE:
255                 return "The PKL and CPL hashes disagree for a picture asset.";
256         case dcp::VerificationNote::SOUND_HASH_INCORRECT:
257                 return dcp::String::compose("The hash of the sound asset %1 does not agree with the PKL file", note.file()->filename());
258         case dcp::VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE:
259                 return "The PKL and CPL hashes disagree for a sound asset.";
260         case dcp::VerificationNote::EMPTY_ASSET_PATH:
261                 return "The asset map contains an empty asset path.";
262         case dcp::VerificationNote::MISSING_ASSET:
263                 return "The file for an asset in the asset map cannot be found.";
264         case dcp::VerificationNote::MISMATCHED_STANDARD:
265                 return "The DCP contains both SMPTE and Interop parts.";
266         case dcp::VerificationNote::BAD_URN_UUID:
267                 return "There is a badly-formed urn:uuid.";
268         case dcp::VerificationNote::BAD_DATE:
269                 return "There is a badly-formed date.";
270         }
271
272         return "";
273 }
274