Catch cxml errors when verifying.
[libdcp.git] / src / verify.cc
1 /*
2     Copyright (C) 2018-2020 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 "reel_subtitle_asset.h"
41 #include "interop_subtitle_asset.h"
42 #include "mono_picture_asset.h"
43 #include "mono_picture_frame.h"
44 #include "stereo_picture_asset.h"
45 #include "stereo_picture_frame.h"
46 #include "exceptions.h"
47 #include "compose.hpp"
48 #include "raw_convert.h"
49 #include <xercesc/util/PlatformUtils.hpp>
50 #include <xercesc/parsers/XercesDOMParser.hpp>
51 #include <xercesc/parsers/AbstractDOMParser.hpp>
52 #include <xercesc/sax/HandlerBase.hpp>
53 #include <xercesc/dom/DOMImplementation.hpp>
54 #include <xercesc/dom/DOMImplementationLS.hpp>
55 #include <xercesc/dom/DOMImplementationRegistry.hpp>
56 #include <xercesc/dom/DOMLSParser.hpp>
57 #include <xercesc/dom/DOMException.hpp>
58 #include <xercesc/dom/DOMDocument.hpp>
59 #include <xercesc/dom/DOMNodeList.hpp>
60 #include <xercesc/dom/DOMError.hpp>
61 #include <xercesc/dom/DOMLocator.hpp>
62 #include <xercesc/dom/DOMNamedNodeMap.hpp>
63 #include <xercesc/dom/DOMAttr.hpp>
64 #include <xercesc/dom/DOMErrorHandler.hpp>
65 #include <xercesc/framework/LocalFileInputSource.hpp>
66 #include <xercesc/framework/MemBufInputSource.hpp>
67 #include <boost/noncopyable.hpp>
68 #include <boost/foreach.hpp>
69 #include <boost/algorithm/string.hpp>
70 #include <map>
71 #include <list>
72 #include <vector>
73 #include <iostream>
74
75 using std::list;
76 using std::vector;
77 using std::string;
78 using std::cout;
79 using std::map;
80 using std::max;
81 using boost::shared_ptr;
82 using boost::optional;
83 using boost::function;
84 using boost::dynamic_pointer_cast;
85
86 using namespace dcp;
87 using namespace xercesc;
88
89 static
90 string
91 xml_ch_to_string (XMLCh const * a)
92 {
93         char* x = XMLString::transcode(a);
94         string const o(x);
95         XMLString::release(&x);
96         return o;
97 }
98
99 class XMLValidationError
100 {
101 public:
102         XMLValidationError (SAXParseException const & e)
103                 : _message (xml_ch_to_string(e.getMessage()))
104                 , _line (e.getLineNumber())
105                 , _column (e.getColumnNumber())
106         {
107
108         }
109
110         string message () const {
111                 return _message;
112         }
113
114         uint64_t line () const {
115                 return _line;
116         }
117
118         uint64_t column () const {
119                 return _column;
120         }
121
122 private:
123         string _message;
124         uint64_t _line;
125         uint64_t _column;
126 };
127
128
129 class DCPErrorHandler : public ErrorHandler
130 {
131 public:
132         void warning(const SAXParseException& e)
133         {
134                 maybe_add (XMLValidationError(e));
135         }
136
137         void error(const SAXParseException& e)
138         {
139                 maybe_add (XMLValidationError(e));
140         }
141
142         void fatalError(const SAXParseException& e)
143         {
144                 maybe_add (XMLValidationError(e));
145         }
146
147         void resetErrors() {
148                 _errors.clear ();
149         }
150
151         list<XMLValidationError> errors () const {
152                 return _errors;
153         }
154
155 private:
156         void maybe_add (XMLValidationError e)
157         {
158                 /* XXX: nasty hack */
159                 if (
160                         e.message().find("schema document") != string::npos &&
161                         e.message().find("has different target namespace from the one specified in instance document") != string::npos
162                         ) {
163                         return;
164                 }
165
166                 _errors.push_back (e);
167         }
168
169         list<XMLValidationError> _errors;
170 };
171
172 class StringToXMLCh : public boost::noncopyable
173 {
174 public:
175         StringToXMLCh (string a)
176         {
177                 _buffer = XMLString::transcode(a.c_str());
178         }
179
180         ~StringToXMLCh ()
181         {
182                 XMLString::release (&_buffer);
183         }
184
185         XMLCh const * get () const {
186                 return _buffer;
187         }
188
189 private:
190         XMLCh* _buffer;
191 };
192
193 class LocalFileResolver : public EntityResolver
194 {
195 public:
196         LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
197                 : _xsd_dtd_directory (xsd_dtd_directory)
198         {
199                 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
200                 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
201                 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
202                 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
203                 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
204                 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
205                 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
206                 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
207                 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
208                 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "SMPTE-428-7-2010-DCST.xsd");
209         }
210
211         InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id)
212         {
213                 string system_id_str = xml_ch_to_string (system_id);
214                 boost::filesystem::path p = _xsd_dtd_directory;
215                 if (_files.find(system_id_str) == _files.end()) {
216                         p /= system_id_str;
217                 } else {
218                         p /= _files[system_id_str];
219                 }
220                 StringToXMLCh ch (p.string());
221                 return new LocalFileInputSource(ch.get());
222         }
223
224 private:
225         void add (string uri, string file)
226         {
227                 _files[uri] = file;
228         }
229
230         std::map<string, string> _files;
231         boost::filesystem::path _xsd_dtd_directory;
232 };
233
234
235 static void
236 parse (XercesDOMParser& parser, boost::filesystem::path xml)
237 {
238         parser.parse(xml.string().c_str());
239 }
240
241
242 static void
243 parse (XercesDOMParser& parser, std::string xml)
244 {
245         xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
246         parser.parse(buf);
247 }
248
249
250 template <class T>
251 void
252 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, list<VerificationNote>& notes)
253 {
254         try {
255                 XMLPlatformUtils::Initialize ();
256         } catch (XMLException& e) {
257                 throw MiscError ("Failed to initialise xerces library");
258         }
259
260         DCPErrorHandler error_handler;
261
262         /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
263         {
264                 XercesDOMParser parser;
265                 parser.setValidationScheme(XercesDOMParser::Val_Always);
266                 parser.setDoNamespaces(true);
267                 parser.setDoSchema(true);
268
269                 vector<string> schema;
270                 schema.push_back("xmldsig-core-schema.xsd");
271                 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
272                 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
273                 schema.push_back("SMPTE-429-9-2007-AM.xsd");
274                 schema.push_back("Main-Stereo-Picture-CPL.xsd");
275                 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
276                 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
277                 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
278                 schema.push_back("DCSubtitle.v1.mattsson.xsd");
279                 schema.push_back("DCDMSubtitle-2010.xsd");
280                 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
281
282                 /* XXX: I'm not especially clear what this is for, but it seems to be necessary */
283                 string locations;
284                 BOOST_FOREACH (string i, schema) {
285                         locations += String::compose("%1 %1 ", i, i);
286                 }
287
288                 parser.setExternalSchemaLocation(locations.c_str());
289                 parser.setValidationSchemaFullChecking(true);
290                 parser.setErrorHandler(&error_handler);
291
292                 LocalFileResolver resolver (xsd_dtd_directory);
293                 parser.setEntityResolver(&resolver);
294
295                 try {
296                         parser.resetDocumentPool();
297                         parse(parser, xml);
298                 } catch (XMLException& e) {
299                         throw MiscError(xml_ch_to_string(e.getMessage()));
300                 } catch (DOMException& e) {
301                         throw MiscError(xml_ch_to_string(e.getMessage()));
302                 } catch (...) {
303                         throw MiscError("Unknown exception from xerces");
304                 }
305         }
306
307         XMLPlatformUtils::Terminate ();
308
309         BOOST_FOREACH (XMLValidationError i, error_handler.errors()) {
310                 notes.push_back (
311                         VerificationNote(
312                                 VerificationNote::VERIFY_ERROR,
313                                 VerificationNote::XML_VALIDATION_ERROR,
314                                 i.message(),
315                                 xml,
316                                 i.line()
317                                 )
318                         );
319         }
320 }
321
322
323 enum VerifyAssetResult {
324         VERIFY_ASSET_RESULT_GOOD,
325         VERIFY_ASSET_RESULT_CPL_PKL_DIFFER,
326         VERIFY_ASSET_RESULT_BAD
327 };
328
329
330 static VerifyAssetResult
331 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
332 {
333         string const actual_hash = reel_mxf->asset_ref()->hash(progress);
334
335         list<shared_ptr<PKL> > pkls = dcp->pkls();
336         /* We've read this DCP in so it must have at least one PKL */
337         DCP_ASSERT (!pkls.empty());
338
339         shared_ptr<Asset> asset = reel_mxf->asset_ref().asset();
340
341         optional<string> pkl_hash;
342         BOOST_FOREACH (shared_ptr<PKL> i, pkls) {
343                 pkl_hash = i->hash (reel_mxf->asset_ref()->id());
344                 if (pkl_hash) {
345                         break;
346                 }
347         }
348
349         DCP_ASSERT (pkl_hash);
350
351         optional<string> cpl_hash = reel_mxf->hash();
352         if (cpl_hash && *cpl_hash != *pkl_hash) {
353                 return VERIFY_ASSET_RESULT_CPL_PKL_DIFFER;
354         }
355
356         if (actual_hash != *pkl_hash) {
357                 return VERIFY_ASSET_RESULT_BAD;
358         }
359
360         return VERIFY_ASSET_RESULT_GOOD;
361 }
362
363
364 enum VerifyPictureAssetResult
365 {
366         VERIFY_PICTURE_ASSET_RESULT_GOOD,
367         VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_BIG,
368         VERIFY_PICTURE_ASSET_RESULT_BAD,
369 };
370
371
372 int
373 biggest_frame_size (shared_ptr<const MonoPictureFrame> frame)
374 {
375         return frame->j2k_size ();
376 }
377
378 int
379 biggest_frame_size (shared_ptr<const StereoPictureFrame> frame)
380 {
381         return max(frame->left_j2k_size(), frame->right_j2k_size());
382 }
383
384
385 template <class A, class R, class F>
386 optional<VerifyPictureAssetResult>
387 verify_picture_asset_type (shared_ptr<ReelMXF> reel_mxf, function<void (float)> progress)
388 {
389         shared_ptr<A> asset = dynamic_pointer_cast<A>(reel_mxf->asset_ref().asset());
390         if (!asset) {
391                 return optional<VerifyPictureAssetResult>();
392         }
393
394         int biggest_frame = 0;
395         shared_ptr<R> reader = asset->start_read ();
396         int64_t const duration = asset->intrinsic_duration ();
397         for (int64_t i = 0; i < duration; ++i) {
398                 shared_ptr<const F> frame = reader->get_frame (i);
399                 biggest_frame = max(biggest_frame, biggest_frame_size(frame));
400                 progress (float(i) / duration);
401         }
402
403         static const int max_frame =   rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
404         static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
405         if (biggest_frame > max_frame) {
406                 return VERIFY_PICTURE_ASSET_RESULT_BAD;
407         } else if (biggest_frame > risky_frame) {
408                 return VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_BIG;
409         }
410
411         return VERIFY_PICTURE_ASSET_RESULT_GOOD;
412 }
413
414
415 static VerifyPictureAssetResult
416 verify_picture_asset (shared_ptr<ReelMXF> reel_mxf, function<void (float)> progress)
417 {
418         optional<VerifyPictureAssetResult> r = verify_picture_asset_type<MonoPictureAsset, MonoPictureAssetReader, MonoPictureFrame>(reel_mxf, progress);
419         if (!r) {
420                 r = verify_picture_asset_type<StereoPictureAsset, StereoPictureAssetReader, StereoPictureFrame>(reel_mxf, progress);
421         }
422
423         DCP_ASSERT (r);
424         return *r;
425 }
426
427
428 static void
429 verify_main_picture_asset (
430         shared_ptr<const DCP> dcp,
431         shared_ptr<const Reel> reel,
432         function<void (string, optional<boost::filesystem::path>)> stage,
433         function<void (float)> progress,
434         list<VerificationNote>& notes
435         )
436 {
437         boost::filesystem::path const file = *reel->main_picture()->asset()->file();
438         stage ("Checking picture asset hash", file);
439         VerifyAssetResult const r = verify_asset (dcp, reel->main_picture(), progress);
440         switch (r) {
441                 case VERIFY_ASSET_RESULT_BAD:
442                         notes.push_back (
443                                 VerificationNote(
444                                         VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_HASH_INCORRECT, file
445                                         )
446                                 );
447                         break;
448                 case VERIFY_ASSET_RESULT_CPL_PKL_DIFFER:
449                         notes.push_back (
450                                 VerificationNote(
451                                         VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE, file
452                                         )
453                                 );
454                         break;
455                 default:
456                         break;
457         }
458         stage ("Checking picture frame sizes", reel->main_picture()->asset()->file());
459         VerifyPictureAssetResult const pr = verify_picture_asset (reel->main_picture(), progress);
460         switch (pr) {
461                 case VERIFY_PICTURE_ASSET_RESULT_BAD:
462                         notes.push_back (
463                                 VerificationNote(
464                                         VerificationNote::VERIFY_ERROR, VerificationNote::PICTURE_FRAME_TOO_LARGE, file
465                                         )
466                                 );
467                         break;
468                 case VERIFY_PICTURE_ASSET_RESULT_FRAME_NEARLY_TOO_BIG:
469                         notes.push_back (
470                                 VerificationNote(
471                                         VerificationNote::VERIFY_WARNING, VerificationNote::PICTURE_FRAME_NEARLY_TOO_LARGE, file
472                                         )
473                                 );
474                         break;
475                 default:
476                         break;
477         }
478 }
479
480
481 static void
482 verify_main_sound_asset (
483         shared_ptr<const DCP> dcp,
484         shared_ptr<const Reel> reel,
485         function<void (string, optional<boost::filesystem::path>)> stage,
486         function<void (float)> progress,
487         list<VerificationNote>& notes
488         )
489 {
490         stage ("Checking sound asset hash", reel->main_sound()->asset()->file());
491         VerifyAssetResult const r = verify_asset (dcp, reel->main_sound(), progress);
492         switch (r) {
493                 case VERIFY_ASSET_RESULT_BAD:
494                         notes.push_back (
495                                 VerificationNote(
496                                         VerificationNote::VERIFY_ERROR, VerificationNote::SOUND_HASH_INCORRECT, *reel->main_sound()->asset()->file()
497                                         )
498                                 );
499                         break;
500                 case VERIFY_ASSET_RESULT_CPL_PKL_DIFFER:
501                         notes.push_back (
502                                 VerificationNote(
503                                         VerificationNote::VERIFY_ERROR, VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE, *reel->main_sound()->asset()->file()
504                                         )
505                                 );
506                         break;
507                 default:
508                         break;
509         }
510 }
511
512
513 static void
514 verify_main_subtitle_asset (
515         shared_ptr<const Reel> reel,
516         function<void (string, optional<boost::filesystem::path>)> stage,
517         boost::filesystem::path xsd_dtd_directory,
518         list<VerificationNote>& notes
519         )
520 {
521         shared_ptr<ReelSubtitleAsset> reel_asset = reel->main_subtitle ();
522         stage ("Checking subtitle XML", reel->main_subtitle()->asset()->file());
523         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
524          * gets passed through libdcp which may clean up and therefore hide errors.
525          */
526         validate_xml (reel->main_subtitle()->asset()->raw_xml(), xsd_dtd_directory, notes);
527 }
528
529
530 list<VerificationNote>
531 dcp::verify (
532         vector<boost::filesystem::path> directories,
533         function<void (string, optional<boost::filesystem::path>)> stage,
534         function<void (float)> progress,
535         boost::filesystem::path xsd_dtd_directory
536         )
537 {
538         xsd_dtd_directory = boost::filesystem::canonical (xsd_dtd_directory);
539
540         list<VerificationNote> notes;
541
542         list<shared_ptr<DCP> > dcps;
543         BOOST_FOREACH (boost::filesystem::path i, directories) {
544                 dcps.push_back (shared_ptr<DCP> (new DCP (i)));
545         }
546
547         BOOST_FOREACH (shared_ptr<DCP> dcp, dcps) {
548                 stage ("Checking DCP", dcp->directory());
549                 try {
550                         dcp->read (&notes);
551                 } catch (ReadError& e) {
552                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
553                 } catch (XMLError& e) {
554                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
555                 } catch (cxml::Error& e) {
556                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::GENERAL_READ, string(e.what())));
557                 }
558
559                 BOOST_FOREACH (shared_ptr<CPL> cpl, dcp->cpls()) {
560                         stage ("Checking CPL", cpl->file());
561                         validate_xml (cpl->file().get(), xsd_dtd_directory, notes);
562
563                         /* Check that the CPL's hash corresponds to the PKL */
564                         BOOST_FOREACH (shared_ptr<PKL> i, dcp->pkls()) {
565                                 optional<string> h = i->hash(cpl->id());
566                                 if (h && make_digest(Data(*cpl->file())) != *h) {
567                                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::CPL_HASH_INCORRECT));
568                                 }
569                         }
570
571                         BOOST_FOREACH (shared_ptr<Reel> reel, cpl->reels()) {
572                                 stage ("Checking reel", optional<boost::filesystem::path>());
573
574                                 BOOST_FOREACH (shared_ptr<ReelAsset> i, reel->assets()) {
575                                         if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
576                                                 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::DURATION_TOO_SMALL, i->id()));
577                                         }
578                                         if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
579                                                 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INTRINSIC_DURATION_TOO_SMALL, i->id()));
580                                         }
581                                 }
582
583                                 if (reel->main_picture()) {
584                                         /* Check reel stuff */
585                                         Fraction const frame_rate = reel->main_picture()->frame_rate();
586                                         if (frame_rate.denominator != 1 ||
587                                             (frame_rate.numerator != 24 &&
588                                              frame_rate.numerator != 25 &&
589                                              frame_rate.numerator != 30 &&
590                                              frame_rate.numerator != 48 &&
591                                              frame_rate.numerator != 50 &&
592                                              frame_rate.numerator != 60 &&
593                                              frame_rate.numerator != 96)) {
594                                                 notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::INVALID_PICTURE_FRAME_RATE));
595                                         }
596                                         /* Check asset */
597                                         if (reel->main_picture()->asset_ref().resolved()) {
598                                                 verify_main_picture_asset (dcp, reel, stage, progress, notes);
599                                         }
600                                 }
601
602                                 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
603                                         verify_main_sound_asset (dcp, reel, stage, progress, notes);
604                                 }
605
606                                 if (reel->main_subtitle() && reel->main_subtitle()->asset_ref().resolved()) {
607                                         verify_main_subtitle_asset (reel, stage, xsd_dtd_directory, notes);
608                                 }
609                         }
610                 }
611
612                 BOOST_FOREACH (shared_ptr<PKL> pkl, dcp->pkls()) {
613                         stage ("Checking PKL", pkl->file());
614                         validate_xml (pkl->file().get(), xsd_dtd_directory, notes);
615                 }
616
617                 if (dcp->asset_map_path()) {
618                         stage ("Checking ASSETMAP", dcp->asset_map_path().get());
619                         validate_xml (dcp->asset_map_path().get(), xsd_dtd_directory, notes);
620                 } else {
621                         notes.push_back (VerificationNote(VerificationNote::VERIFY_ERROR, VerificationNote::MISSING_ASSETMAP));
622                 }
623         }
624
625         return notes;
626 }
627
628 string
629 dcp::note_to_string (dcp::VerificationNote note)
630 {
631         switch (note.code()) {
632         case dcp::VerificationNote::GENERAL_READ:
633                 return *note.note();
634         case dcp::VerificationNote::CPL_HASH_INCORRECT:
635                 return "The hash of the CPL in the PKL does not agree with the CPL file.";
636         case dcp::VerificationNote::INVALID_PICTURE_FRAME_RATE:
637                 return "The picture in a reel has an invalid frame rate.";
638         case dcp::VerificationNote::PICTURE_HASH_INCORRECT:
639                 return dcp::String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
640         case dcp::VerificationNote::PKL_CPL_PICTURE_HASHES_DISAGREE:
641                 return dcp::String::compose("The PKL and CPL hashes disagree for the picture asset %1.", note.file()->filename());
642         case dcp::VerificationNote::SOUND_HASH_INCORRECT:
643                 return dcp::String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
644         case dcp::VerificationNote::PKL_CPL_SOUND_HASHES_DISAGREE:
645                 return dcp::String::compose("The PKL and CPL hashes disagree for the sound asset %1.", note.file()->filename());
646         case dcp::VerificationNote::EMPTY_ASSET_PATH:
647                 return "The asset map contains an empty asset path.";
648         case dcp::VerificationNote::MISSING_ASSET:
649                 return String::compose("The file for an asset in the asset map cannot be found; missing file is %1.", note.file()->filename());
650         case dcp::VerificationNote::MISMATCHED_STANDARD:
651                 return "The DCP contains both SMPTE and Interop parts.";
652         case dcp::VerificationNote::XML_VALIDATION_ERROR:
653                 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
654         case dcp::VerificationNote::MISSING_ASSETMAP:
655                 return "No ASSETMAP or ASSETMAP.xml was found.";
656         case dcp::VerificationNote::INTRINSIC_DURATION_TOO_SMALL:
657                 return String::compose("The intrinsic duration of an asset is less than 1 second long: %1", note.note().get());
658         case dcp::VerificationNote::DURATION_TOO_SMALL:
659                 return String::compose("The duration of an asset is less than 1 second long: %1", note.note().get());
660         case dcp::VerificationNote::PICTURE_FRAME_TOO_LARGE:
661                 return String::compose("The instantaneous bit rate of the picture asset %1 is larger than the limit of 250Mbit/s in at least one place.", note.file()->filename());
662         case dcp::VerificationNote::PICTURE_FRAME_NEARLY_TOO_LARGE:
663                 return String::compose("The instantaneous bit rate of the picture asset %1 is close to the limit of 250Mbit/s in at least one place.", note.file()->filename());
664         case dcp::VerificationNote::EXTERNAL_ASSET:
665                 return String::compose("An asset that this DCP refers to is not included in the DCP.  It may be a VF.  Missing asset is %1.", note.note().get());
666         }
667
668         return "";
669 }