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