fe071b48ce39353a566acc1836e28e848f19e6ff
[libdcp.git] / src / verify.cc
1 /*
2     Copyright (C) 2018-2021 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 "reel_markers_asset.h"
51 #include "smpte_subtitle_asset.h"
52 #include <xercesc/util/PlatformUtils.hpp>
53 #include <xercesc/parsers/XercesDOMParser.hpp>
54 #include <xercesc/parsers/AbstractDOMParser.hpp>
55 #include <xercesc/sax/HandlerBase.hpp>
56 #include <xercesc/dom/DOMImplementation.hpp>
57 #include <xercesc/dom/DOMImplementationLS.hpp>
58 #include <xercesc/dom/DOMImplementationRegistry.hpp>
59 #include <xercesc/dom/DOMLSParser.hpp>
60 #include <xercesc/dom/DOMException.hpp>
61 #include <xercesc/dom/DOMDocument.hpp>
62 #include <xercesc/dom/DOMNodeList.hpp>
63 #include <xercesc/dom/DOMError.hpp>
64 #include <xercesc/dom/DOMLocator.hpp>
65 #include <xercesc/dom/DOMNamedNodeMap.hpp>
66 #include <xercesc/dom/DOMAttr.hpp>
67 #include <xercesc/dom/DOMErrorHandler.hpp>
68 #include <xercesc/framework/LocalFileInputSource.hpp>
69 #include <xercesc/framework/MemBufInputSource.hpp>
70 #include <boost/noncopyable.hpp>
71 #include <boost/algorithm/string.hpp>
72 #include <map>
73 #include <vector>
74 #include <iostream>
75
76 using std::list;
77 using std::vector;
78 using std::string;
79 using std::cout;
80 using std::map;
81 using std::max;
82 using std::set;
83 using std::shared_ptr;
84 using std::make_shared;
85 using boost::optional;
86 using boost::function;
87 using std::dynamic_pointer_cast;
88
89 using namespace dcp;
90 using namespace xercesc;
91
92 static
93 string
94 xml_ch_to_string (XMLCh const * a)
95 {
96         char* x = XMLString::transcode(a);
97         string const o(x);
98         XMLString::release(&x);
99         return o;
100 }
101
102 class XMLValidationError
103 {
104 public:
105         XMLValidationError (SAXParseException const & e)
106                 : _message (xml_ch_to_string(e.getMessage()))
107                 , _line (e.getLineNumber())
108                 , _column (e.getColumnNumber())
109                 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
110                 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
111         {
112
113         }
114
115         string message () const {
116                 return _message;
117         }
118
119         uint64_t line () const {
120                 return _line;
121         }
122
123         uint64_t column () const {
124                 return _column;
125         }
126
127         string public_id () const {
128                 return _public_id;
129         }
130
131         string system_id () const {
132                 return _system_id;
133         }
134
135 private:
136         string _message;
137         uint64_t _line;
138         uint64_t _column;
139         string _public_id;
140         string _system_id;
141 };
142
143
144 class DCPErrorHandler : public ErrorHandler
145 {
146 public:
147         void warning(const SAXParseException& e)
148         {
149                 maybe_add (XMLValidationError(e));
150         }
151
152         void error(const SAXParseException& e)
153         {
154                 maybe_add (XMLValidationError(e));
155         }
156
157         void fatalError(const SAXParseException& e)
158         {
159                 maybe_add (XMLValidationError(e));
160         }
161
162         void resetErrors() {
163                 _errors.clear ();
164         }
165
166         list<XMLValidationError> errors () const {
167                 return _errors;
168         }
169
170 private:
171         void maybe_add (XMLValidationError e)
172         {
173                 /* XXX: nasty hack */
174                 if (
175                         e.message().find("schema document") != string::npos &&
176                         e.message().find("has different target namespace from the one specified in instance document") != string::npos
177                         ) {
178                         return;
179                 }
180
181                 _errors.push_back (e);
182         }
183
184         list<XMLValidationError> _errors;
185 };
186
187 class StringToXMLCh : public boost::noncopyable
188 {
189 public:
190         StringToXMLCh (string a)
191         {
192                 _buffer = XMLString::transcode(a.c_str());
193         }
194
195         ~StringToXMLCh ()
196         {
197                 XMLString::release (&_buffer);
198         }
199
200         XMLCh const * get () const {
201                 return _buffer;
202         }
203
204 private:
205         XMLCh* _buffer;
206 };
207
208 class LocalFileResolver : public EntityResolver
209 {
210 public:
211         LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
212                 : _xsd_dtd_directory (xsd_dtd_directory)
213         {
214                 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
215                  * found without being here.
216                  */
217                 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
218                 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
219                 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
220                 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
221                 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
222                 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
223                 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
224                 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
225                 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
226                 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "SMPTE-428-7-2010-DCST.xsd");
227                 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
228                 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
229                 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
230         }
231
232         InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id)
233         {
234                 if (!system_id) {
235                         return 0;
236                 }
237                 auto system_id_str = xml_ch_to_string (system_id);
238                 auto p = _xsd_dtd_directory;
239                 if (_files.find(system_id_str) == _files.end()) {
240                         p /= system_id_str;
241                 } else {
242                         p /= _files[system_id_str];
243                 }
244                 StringToXMLCh ch (p.string());
245                 return new LocalFileInputSource(ch.get());
246         }
247
248 private:
249         void add (string uri, string file)
250         {
251                 _files[uri] = file;
252         }
253
254         std::map<string, string> _files;
255         boost::filesystem::path _xsd_dtd_directory;
256 };
257
258
259 static void
260 parse (XercesDOMParser& parser, boost::filesystem::path xml)
261 {
262         parser.parse(xml.string().c_str());
263 }
264
265
266 static void
267 parse (XercesDOMParser& parser, string xml)
268 {
269         xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
270         parser.parse(buf);
271 }
272
273
274 template <class T>
275 void
276 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
277 {
278         try {
279                 XMLPlatformUtils::Initialize ();
280         } catch (XMLException& e) {
281                 throw MiscError ("Failed to initialise xerces library");
282         }
283
284         DCPErrorHandler error_handler;
285
286         /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
287         {
288                 XercesDOMParser parser;
289                 parser.setValidationScheme(XercesDOMParser::Val_Always);
290                 parser.setDoNamespaces(true);
291                 parser.setDoSchema(true);
292
293                 vector<string> schema;
294                 schema.push_back("xml.xsd");
295                 schema.push_back("xmldsig-core-schema.xsd");
296                 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
297                 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
298                 schema.push_back("SMPTE-429-9-2007-AM.xsd");
299                 schema.push_back("Main-Stereo-Picture-CPL.xsd");
300                 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
301                 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
302                 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
303                 schema.push_back("DCSubtitle.v1.mattsson.xsd");
304                 schema.push_back("DCDMSubtitle-2010.xsd");
305                 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
306                 schema.push_back("SMPTE-429-16.xsd");
307                 schema.push_back("Dolby-2012-AD.xsd");
308                 schema.push_back("SMPTE-429-10-2008.xsd");
309                 schema.push_back("xlink.xsd");
310                 schema.push_back("SMPTE-335-2012.xsd");
311                 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
312                 schema.push_back("isdcf-mca.xsd");
313                 schema.push_back("SMPTE-429-12-2008.xsd");
314
315                 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
316                  * Schemas that are not mentioned in this list are not read, and the things
317                  * they describe are not checked.
318                  */
319                 string locations;
320                 for (auto i: schema) {
321                         locations += String::compose("%1 %1 ", i, i);
322                 }
323
324                 parser.setExternalSchemaLocation(locations.c_str());
325                 parser.setValidationSchemaFullChecking(true);
326                 parser.setErrorHandler(&error_handler);
327
328                 LocalFileResolver resolver (xsd_dtd_directory);
329                 parser.setEntityResolver(&resolver);
330
331                 try {
332                         parser.resetDocumentPool();
333                         parse(parser, xml);
334                 } catch (XMLException& e) {
335                         throw MiscError(xml_ch_to_string(e.getMessage()));
336                 } catch (DOMException& e) {
337                         throw MiscError(xml_ch_to_string(e.getMessage()));
338                 } catch (...) {
339                         throw MiscError("Unknown exception from xerces");
340                 }
341         }
342
343         XMLPlatformUtils::Terminate ();
344
345         for (auto i: error_handler.errors()) {
346                 notes.push_back ({
347                         VerificationNote::Type::ERROR,
348                         VerificationNote::Code::INVALID_XML,
349                         i.message(),
350                         boost::trim_copy(i.public_id() + " " + i.system_id()),
351                         i.line()
352                 });
353         }
354 }
355
356
357 enum class VerifyAssetResult {
358         GOOD,
359         CPL_PKL_DIFFER,
360         BAD
361 };
362
363
364 static VerifyAssetResult
365 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
366 {
367         auto const actual_hash = reel_mxf->asset_ref()->hash(progress);
368
369         auto pkls = dcp->pkls();
370         /* We've read this DCP in so it must have at least one PKL */
371         DCP_ASSERT (!pkls.empty());
372
373         auto asset = reel_mxf->asset_ref().asset();
374
375         optional<string> pkl_hash;
376         for (auto i: pkls) {
377                 pkl_hash = i->hash (reel_mxf->asset_ref()->id());
378                 if (pkl_hash) {
379                         break;
380                 }
381         }
382
383         DCP_ASSERT (pkl_hash);
384
385         auto cpl_hash = reel_mxf->hash();
386         if (cpl_hash && *cpl_hash != *pkl_hash) {
387                 return VerifyAssetResult::CPL_PKL_DIFFER;
388         }
389
390         if (actual_hash != *pkl_hash) {
391                 return VerifyAssetResult::BAD;
392         }
393
394         return VerifyAssetResult::GOOD;
395 }
396
397
398 void
399 verify_language_tag (string tag, vector<VerificationNote>& notes)
400 {
401         try {
402                 LanguageTag test (tag);
403         } catch (LanguageTagError &) {
404                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
405         }
406 }
407
408
409 enum class VerifyPictureAssetResult
410 {
411         GOOD,
412         FRAME_NEARLY_TOO_LARGE,
413         BAD,
414 };
415
416
417 int
418 biggest_frame_size (shared_ptr<const MonoPictureFrame> frame)
419 {
420         return frame->size ();
421 }
422
423 int
424 biggest_frame_size (shared_ptr<const StereoPictureFrame> frame)
425 {
426         return max(frame->left()->size(), frame->right()->size());
427 }
428
429
430 template <class A, class R, class F>
431 optional<VerifyPictureAssetResult>
432 verify_picture_asset_type (shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
433 {
434         auto asset = dynamic_pointer_cast<A>(reel_mxf->asset_ref().asset());
435         if (!asset) {
436                 return optional<VerifyPictureAssetResult>();
437         }
438
439         int biggest_frame = 0;
440         auto reader = asset->start_read ();
441         auto const duration = asset->intrinsic_duration ();
442         for (int64_t i = 0; i < duration; ++i) {
443                 shared_ptr<const F> frame = reader->get_frame (i);
444                 biggest_frame = max(biggest_frame, biggest_frame_size(frame));
445                 progress (float(i) / duration);
446         }
447
448         static const int max_frame =   rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
449         static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
450         if (biggest_frame > max_frame) {
451                 return VerifyPictureAssetResult::BAD;
452         } else if (biggest_frame > risky_frame) {
453                 return VerifyPictureAssetResult::FRAME_NEARLY_TOO_LARGE;
454         }
455
456         return VerifyPictureAssetResult::GOOD;
457 }
458
459
460 static VerifyPictureAssetResult
461 verify_picture_asset (shared_ptr<const ReelMXF> reel_mxf, function<void (float)> progress)
462 {
463         auto r = verify_picture_asset_type<MonoPictureAsset, MonoPictureAssetReader, MonoPictureFrame>(reel_mxf, progress);
464         if (!r) {
465                 r = verify_picture_asset_type<StereoPictureAsset, StereoPictureAssetReader, StereoPictureFrame>(reel_mxf, progress);
466         }
467
468         DCP_ASSERT (r);
469         return *r;
470 }
471
472
473 static void
474 verify_main_picture_asset (
475         shared_ptr<const DCP> dcp,
476         shared_ptr<const ReelPictureAsset> reel_asset,
477         function<void (string, optional<boost::filesystem::path>)> stage,
478         function<void (float)> progress,
479         vector<VerificationNote>& notes
480         )
481 {
482         auto asset = reel_asset->asset();
483         auto const file = *asset->file();
484         stage ("Checking picture asset hash", file);
485         auto const r = verify_asset (dcp, reel_asset, progress);
486         switch (r) {
487                 case VerifyAssetResult::BAD:
488                         notes.push_back ({
489                                 VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
490                         });
491                         break;
492                 case VerifyAssetResult::CPL_PKL_DIFFER:
493                         notes.push_back ({
494                                 VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
495                         });
496                         break;
497                 default:
498                         break;
499         }
500         stage ("Checking picture frame sizes", asset->file());
501         auto const pr = verify_picture_asset (reel_asset, progress);
502         switch (pr) {
503                 case VerifyPictureAssetResult::BAD:
504                         notes.push_back ({
505                                 VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
506                         });
507                         break;
508                 case VerifyPictureAssetResult::FRAME_NEARLY_TOO_LARGE:
509                         notes.push_back ({
510                                 VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
511                         });
512                         break;
513                 default:
514                         break;
515         }
516
517         /* Only flat/scope allowed by Bv2.1 */
518         if (
519                 asset->size() != Size(2048, 858) &&
520                 asset->size() != Size(1998, 1080) &&
521                 asset->size() != Size(4096, 1716) &&
522                 asset->size() != Size(3996, 2160)) {
523                 notes.push_back({
524                         VerificationNote::Type::BV21_ERROR,
525                         VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
526                         String::compose("%1x%2", asset->size().width, asset->size().height),
527                         file
528                 });
529         }
530
531         /* Only 24, 25, 48fps allowed for 2K */
532         if (
533                 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
534                 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
535            ) {
536                 notes.push_back({
537                         VerificationNote::Type::BV21_ERROR,
538                         VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
539                         String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
540                         file
541                 });
542         }
543
544         if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
545                 /* Only 24fps allowed for 4K */
546                 if (asset->edit_rate() != Fraction(24, 1)) {
547                         notes.push_back({
548                                 VerificationNote::Type::BV21_ERROR,
549                                 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
550                                 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
551                                 file
552                         });
553                 }
554
555                 /* Only 2D allowed for 4K */
556                 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
557                         notes.push_back({
558                                 VerificationNote::Type::BV21_ERROR,
559                                 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
560                                 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
561                                 file
562                         });
563
564                 }
565         }
566
567 }
568
569
570 static void
571 verify_main_sound_asset (
572         shared_ptr<const DCP> dcp,
573         shared_ptr<const ReelSoundAsset> reel_asset,
574         function<void (string, optional<boost::filesystem::path>)> stage,
575         function<void (float)> progress,
576         vector<VerificationNote>& notes
577         )
578 {
579         auto asset = reel_asset->asset();
580         stage ("Checking sound asset hash", asset->file());
581         auto const r = verify_asset (dcp, reel_asset, progress);
582         switch (r) {
583                 case VerifyAssetResult::BAD:
584                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, *asset->file()});
585                         break;
586                 case VerifyAssetResult::CPL_PKL_DIFFER:
587                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, *asset->file()});
588                         break;
589                 default:
590                         break;
591         }
592
593         stage ("Checking sound asset metadata", asset->file());
594
595         verify_language_tag (asset->language(), notes);
596         if (asset->sampling_rate() != 48000) {
597                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), *asset->file()});
598         }
599 }
600
601
602 static void
603 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
604 {
605         /* XXX: is Language compulsory? */
606         if (reel_asset->language()) {
607                 verify_language_tag (*reel_asset->language(), notes);
608         }
609
610         if (!reel_asset->entry_point()) {
611                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
612         } else if (reel_asset->entry_point().get()) {
613                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
614         }
615 }
616
617
618 static void
619 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
620 {
621         /* XXX: is Language compulsory? */
622         if (reel_asset->language()) {
623                 verify_language_tag (*reel_asset->language(), notes);
624         }
625
626         if (!reel_asset->entry_point()) {
627                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
628         } else if (reel_asset->entry_point().get()) {
629                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
630         }
631 }
632
633
634 struct State
635 {
636         boost::optional<string> subtitle_language;
637 };
638
639
640
641 void
642 verify_smpte_subtitle_asset (
643         shared_ptr<const SMPTESubtitleAsset> asset,
644         vector<VerificationNote>& notes,
645         State& state
646         )
647 {
648         if (asset->language()) {
649                 auto const language = *asset->language();
650                 verify_language_tag (language, notes);
651                 if (!state.subtitle_language) {
652                         state.subtitle_language = language;
653                 } else if (state.subtitle_language != language) {
654                         notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
655                 }
656         } else {
657                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
658         }
659         auto const size = boost::filesystem::file_size(asset->file().get());
660         if (size > 115 * 1024 * 1024) {
661                 notes.push_back (
662                         { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
663                         );
664         }
665         /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
666          * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
667          */
668         auto fonts = asset->font_data ();
669         int total_size = 0;
670         for (auto i: fonts) {
671                 total_size += i.second.size();
672         }
673         if (total_size > 10 * 1024 * 1024) {
674                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
675         }
676
677         if (!asset->start_time()) {
678                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
679         } else if (asset->start_time() != Time()) {
680                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
681         }
682 }
683
684
685 static void
686 verify_subtitle_asset (
687         shared_ptr<const SubtitleAsset> asset,
688         function<void (string, optional<boost::filesystem::path>)> stage,
689         boost::filesystem::path xsd_dtd_directory,
690         vector<VerificationNote>& notes,
691         State& state
692         )
693 {
694         stage ("Checking subtitle XML", asset->file());
695         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
696          * gets passed through libdcp which may clean up and therefore hide errors.
697          */
698         validate_xml (asset->raw_xml(), xsd_dtd_directory, notes);
699
700         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
701         if (smpte) {
702                 verify_smpte_subtitle_asset (smpte, notes, state);
703         }
704 }
705
706
707 static void
708 verify_closed_caption_asset (
709         shared_ptr<const SubtitleAsset> asset,
710         function<void (string, optional<boost::filesystem::path>)> stage,
711         boost::filesystem::path xsd_dtd_directory,
712         vector<VerificationNote>& notes,
713         State& state
714         )
715 {
716         verify_subtitle_asset (asset, stage, xsd_dtd_directory, notes, state);
717
718         if (asset->raw_xml().size() > 256 * 1024) {
719                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(asset->raw_xml().size()), *asset->file()});
720         }
721 }
722
723
724 static
725 void
726 verify_text_timing (
727         vector<shared_ptr<Reel>> reels,
728         optional<int> picture_frame_rate,
729         vector<VerificationNote>& notes,
730         std::function<bool (shared_ptr<Reel>)> check,
731         std::function<string (shared_ptr<Reel>)> xml,
732         std::function<int64_t (shared_ptr<Reel>)> duration
733         )
734 {
735         /* end of last subtitle (in editable units) */
736         optional<int64_t> last_out;
737         auto too_short = false;
738         auto too_close = false;
739         auto too_early = false;
740         /* current reel start time (in editable units) */
741         int64_t reel_offset = 0;
742
743         std::function<void (cxml::ConstNodePtr, int, int, bool)> parse;
744         parse = [&parse, &last_out, &too_short, &too_close, &too_early, &reel_offset](cxml::ConstNodePtr node, int tcr, int pfr, bool first_reel) {
745                 if (node->name() == "Subtitle") {
746                         Time in (node->string_attribute("TimeIn"), tcr);
747                         Time out (node->string_attribute("TimeOut"), tcr);
748                         if (first_reel && in < Time(0, 0, 4, 0, tcr)) {
749                                 too_early = true;
750                         }
751                         auto length = out - in;
752                         if (length.as_editable_units(pfr) < 15) {
753                                 too_short = true;
754                         }
755                         if (last_out) {
756                                 /* XXX: this feels dubious - is it really what Bv2.1 means? */
757                                 auto distance = reel_offset + in.as_editable_units(pfr) - *last_out;
758                                 if (distance >= 0 && distance < 2) {
759                                         too_close = true;
760                                 }
761                         }
762                         last_out = reel_offset + out.as_editable_units(pfr);
763                 } else {
764                         for (auto i: node->node_children()) {
765                                 parse(i, tcr, pfr, first_reel);
766                         }
767                 }
768         };
769
770         for (auto i = 0U; i < reels.size(); ++i) {
771                 if (!check(reels[i])) {
772                         continue;
773                 }
774
775                 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
776                  * read in by libdcp's parser.
777                  */
778
779                 auto doc = make_shared<cxml::Document>("SubtitleReel");
780                 doc->read_string (xml(reels[i]));
781                 auto const tcr = doc->number_child<int>("TimeCodeRate");
782                 parse (doc, tcr, picture_frame_rate.get_value_or(24), i == 0);
783                 reel_offset += duration(reels[i]);
784         }
785
786         if (too_early) {
787                 notes.push_back({
788                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
789                 });
790         }
791
792         if (too_short) {
793                 notes.push_back ({
794                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
795                 });
796         }
797
798         if (too_close) {
799                 notes.push_back ({
800                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
801                 });
802         }
803 }
804
805
806 struct LinesCharactersResult
807 {
808         bool warning_length_exceeded = false;
809         bool error_length_exceeded = false;
810         bool line_count_exceeded = false;
811 };
812
813
814 static
815 void
816 verify_text_lines_and_characters (
817         shared_ptr<SubtitleAsset> asset,
818         int warning_length,
819         int error_length,
820         LinesCharactersResult* result
821         )
822 {
823         class Event
824         {
825         public:
826                 Event (Time time_, float position_, int characters_)
827                         : time (time_)
828                         , position (position_)
829                         , characters (characters_)
830                 {}
831
832                 Event (Time time_, shared_ptr<Event> start_)
833                         : time (time_)
834                         , start (start_)
835                 {}
836
837                 Time time;
838                 int position; //< position from 0 at top of screen to 100 at bottom
839                 int characters;
840                 shared_ptr<Event> start;
841         };
842
843         vector<shared_ptr<Event>> events;
844
845         auto position = [](shared_ptr<const SubtitleString> sub) {
846                 switch (sub->v_align()) {
847                 case VAlign::TOP:
848                         return lrintf(sub->v_position() * 100);
849                 case VAlign::CENTER:
850                         return lrintf((0.5f + sub->v_position()) * 100);
851                 case VAlign::BOTTOM:
852                         return lrintf((1.0f - sub->v_position()) * 100);
853                 }
854
855                 return 0L;
856         };
857
858         for (auto j: asset->subtitles()) {
859                 auto text = dynamic_pointer_cast<const SubtitleString>(j);
860                 if (text) {
861                         auto in = make_shared<Event>(text->in(), position(text), text->text().length());
862                         events.push_back(in);
863                         events.push_back(make_shared<Event>(text->out(), in));
864                 }
865         }
866
867         std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
868                 return a->time < b->time;
869         });
870
871         map<int, int> current;
872         for (auto i: events) {
873                 if (current.size() > 3) {
874                         result->line_count_exceeded = true;
875                 }
876                 for (auto j: current) {
877                         if (j.second >= warning_length) {
878                                 result->warning_length_exceeded = true;
879                         }
880                         if (j.second >= error_length) {
881                                 result->error_length_exceeded = true;
882                         }
883                 }
884
885                 if (i->start) {
886                         /* end of a subtitle */
887                         DCP_ASSERT (current.find(i->start->position) != current.end());
888                         if (current[i->start->position] == i->start->characters) {
889                                 current.erase(i->start->position);
890                         } else {
891                                 current[i->start->position] -= i->start->characters;
892                         }
893                 } else {
894                         /* start of a subtitle */
895                         if (current.find(i->position) == current.end()) {
896                                 current[i->position] = i->characters;
897                         } else {
898                                 current[i->position] += i->characters;
899                         }
900                 }
901         }
902 }
903
904
905 static
906 void
907 verify_text_timing (vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
908 {
909         if (reels.empty()) {
910                 return;
911         }
912
913         optional<int> picture_frame_rate;
914         if (reels[0]->main_picture()) {
915                 picture_frame_rate = reels[0]->main_picture()->frame_rate().numerator;
916         }
917
918         if (reels[0]->main_subtitle()) {
919                 verify_text_timing (reels, picture_frame_rate, notes,
920                         [](shared_ptr<Reel> reel) {
921                                 return static_cast<bool>(reel->main_subtitle());
922                         },
923                         [](shared_ptr<Reel> reel) {
924                                 return reel->main_subtitle()->asset()->raw_xml();
925                         },
926                         [](shared_ptr<Reel> reel) {
927                                 return reel->main_subtitle()->actual_duration();
928                         }
929                 );
930         }
931
932         for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
933                 verify_text_timing (reels, picture_frame_rate, notes,
934                         [i](shared_ptr<Reel> reel) {
935                                 return i < reel->closed_captions().size();
936                         },
937                         [i](shared_ptr<Reel> reel) {
938                                 return reel->closed_captions()[i]->asset()->raw_xml();
939                         },
940                         [i](shared_ptr<Reel> reel) {
941                                 return reel->closed_captions()[i]->actual_duration();
942                         }
943                 );
944         }
945 }
946
947
948 void
949 verify_extension_metadata (shared_ptr<CPL> cpl, vector<VerificationNote>& notes)
950 {
951         DCP_ASSERT (cpl->file());
952         cxml::Document doc ("CompositionPlaylist");
953         doc.read_file (cpl->file().get());
954
955         auto missing = false;
956         string malformed;
957
958         if (auto reel_list = doc.node_child("ReelList")) {
959                 auto reels = reel_list->node_children("Reel");
960                 if (!reels.empty()) {
961                         if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
962                                 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
963                                         if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
964                                                 missing = true;
965                                                 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
966                                                         if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
967                                                                 continue;
968                                                         }
969                                                         missing = false;
970                                                         if (auto name = extension->optional_node_child("Name")) {
971                                                                 if (name->content() != "Application") {
972                                                                         malformed = "<Name> should be 'Application'";
973                                                                 }
974                                                         }
975                                                         if (auto property_list = extension->optional_node_child("PropertyList")) {
976                                                                 if (auto property = property_list->optional_node_child("Property")) {
977                                                                         if (auto name = property->optional_node_child("Name")) {
978                                                                                 if (name->content() != "DCP Constraints Profile") {
979                                                                                         malformed = "<Name> property should be 'DCP Constraints Profile'";
980                                                                                 }
981                                                                         }
982                                                                         if (auto value = property->optional_node_child("Value")) {
983                                                                                 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
984                                                                                         malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
985                                                                                 }
986                                                                         }
987                                                                 }
988                                                         }
989                                                 }
990                                         } else {
991                                                 missing = true;
992                                         }
993                                 }
994                         }
995                 }
996         }
997
998         if (missing) {
999                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1000         } else if (!malformed.empty()) {
1001                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1002         }
1003 }
1004
1005
1006 bool
1007 pkl_has_encrypted_assets (shared_ptr<DCP> dcp, shared_ptr<PKL> pkl)
1008 {
1009         vector<string> encrypted;
1010         for (auto i: dcp->cpls()) {
1011                 for (auto j: i->reel_mxfs()) {
1012                         if (j->asset_ref().resolved()) {
1013                                 /* It's a bit surprising / broken but Interop subtitle assets are represented
1014                                  * in reels by ReelSubtitleAsset which inherits ReelMXF, so it's possible for
1015                                  * ReelMXFs to have assets which are not MXFs.
1016                                  */
1017                                 if (auto asset = dynamic_pointer_cast<MXF>(j->asset_ref().asset())) {
1018                                         if (asset->encrypted()) {
1019                                                 encrypted.push_back(j->asset_ref().id());
1020                                         }
1021                                 }
1022                         }
1023                 }
1024         }
1025
1026         for (auto i: pkl->asset_list()) {
1027                 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1028                         return true;
1029                 }
1030         }
1031
1032         return false;
1033 }
1034
1035
1036 vector<VerificationNote>
1037 dcp::verify (
1038         vector<boost::filesystem::path> directories,
1039         function<void (string, optional<boost::filesystem::path>)> stage,
1040         function<void (float)> progress,
1041         boost::filesystem::path xsd_dtd_directory
1042         )
1043 {
1044         xsd_dtd_directory = boost::filesystem::canonical (xsd_dtd_directory);
1045
1046         vector<VerificationNote> notes;
1047         State state{};
1048
1049         vector<shared_ptr<DCP>> dcps;
1050         for (auto i: directories) {
1051                 dcps.push_back (shared_ptr<DCP> (new DCP (i)));
1052         }
1053
1054         for (auto dcp: dcps) {
1055                 stage ("Checking DCP", dcp->directory());
1056                 try {
1057                         dcp->read (&notes);
1058                 } catch (ReadError& e) {
1059                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1060                 } catch (XMLError& e) {
1061                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1062                 } catch (MXFFileError& e) {
1063                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1064                 } catch (cxml::Error& e) {
1065                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1066                 }
1067
1068                 if (dcp->standard() != Standard::SMPTE) {
1069                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1070                 }
1071
1072                 for (auto cpl: dcp->cpls()) {
1073                         stage ("Checking CPL", cpl->file());
1074                         validate_xml (cpl->file().get(), xsd_dtd_directory, notes);
1075
1076                         if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1077                                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1078                         }
1079
1080                         for (auto const& i: cpl->additional_subtitle_languages()) {
1081                                 verify_language_tag (i, notes);
1082                         }
1083
1084                         if (cpl->release_territory()) {
1085                                 if (!cpl->release_territory_scope() || cpl->release_territory_scope().get() != "http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata#scope/release-territory/UNM49") {
1086                                         auto terr = cpl->release_territory().get();
1087                                         /* Must be a valid region tag, or "001" */
1088                                         try {
1089                                                 LanguageTag::RegionSubtag test (terr);
1090                                         } catch (...) {
1091                                                 if (terr != "001") {
1092                                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1093                                                 }
1094                                         }
1095                                 }
1096                         }
1097
1098                         if (dcp->standard() == Standard::SMPTE) {
1099                                 if (!cpl->annotation_text()) {
1100                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1101                                 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1102                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1103                                 }
1104                         }
1105
1106                         for (auto i: dcp->pkls()) {
1107                                 /* Check that the CPL's hash corresponds to the PKL */
1108                                 optional<string> h = i->hash(cpl->id());
1109                                 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1110                                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1111                                 }
1112
1113                                 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1114                                 optional<string> required_annotation_text;
1115                                 for (auto j: i->asset_list()) {
1116                                         /* See if this is a CPL */
1117                                         for (auto k: dcp->cpls()) {
1118                                                 if (j->id() == k->id()) {
1119                                                         if (!required_annotation_text) {
1120                                                                 /* First CPL we have found; this is the required AnnotationText unless we find another */
1121                                                                 required_annotation_text = cpl->content_title_text();
1122                                                         } else {
1123                                                                 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1124                                                                 required_annotation_text = boost::none;
1125                                                         }
1126                                                 }
1127                                         }
1128                                 }
1129
1130                                 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1131                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1132                                 }
1133                         }
1134
1135                         /* set to true if any reel has a MainSubtitle */
1136                         auto have_main_subtitle = false;
1137                         /* set to true if any reel has no MainSubtitle */
1138                         auto have_no_main_subtitle = false;
1139                         /* fewest number of closed caption assets seen in a reel */
1140                         size_t fewest_closed_captions = SIZE_MAX;
1141                         /* most number of closed caption assets seen in a reel */
1142                         size_t most_closed_captions = 0;
1143                         map<Marker, Time> markers_seen;
1144
1145                         for (auto reel: cpl->reels()) {
1146                                 stage ("Checking reel", optional<boost::filesystem::path>());
1147
1148                                 for (auto i: reel->assets()) {
1149                                         if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1150                                                 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1151                                         }
1152                                         if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1153                                                 notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1154                                         }
1155                                         auto mxf = dynamic_pointer_cast<ReelMXF>(i);
1156                                         if (mxf && !mxf->hash()) {
1157                                                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1158                                         }
1159                                 }
1160
1161                                 if (dcp->standard() == Standard::SMPTE) {
1162                                         boost::optional<int64_t> duration;
1163                                         for (auto i: reel->assets()) {
1164                                                 if (!duration) {
1165                                                         duration = i->actual_duration();
1166                                                 } else if (*duration != i->actual_duration()) {
1167                                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1168                                                         break;
1169                                                 }
1170                                         }
1171                                 }
1172
1173                                 if (reel->main_picture()) {
1174                                         /* Check reel stuff */
1175                                         auto const frame_rate = reel->main_picture()->frame_rate();
1176                                         if (frame_rate.denominator != 1 ||
1177                                             (frame_rate.numerator != 24 &&
1178                                              frame_rate.numerator != 25 &&
1179                                              frame_rate.numerator != 30 &&
1180                                              frame_rate.numerator != 48 &&
1181                                              frame_rate.numerator != 50 &&
1182                                              frame_rate.numerator != 60 &&
1183                                              frame_rate.numerator != 96)) {
1184                                                 notes.push_back ({
1185                                                         VerificationNote::Type::ERROR,
1186                                                         VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1187                                                         String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1188                                                 });
1189                                         }
1190                                         /* Check asset */
1191                                         if (reel->main_picture()->asset_ref().resolved()) {
1192                                                 verify_main_picture_asset (dcp, reel->main_picture(), stage, progress, notes);
1193                                         }
1194                                 }
1195
1196                                 if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1197                                         verify_main_sound_asset (dcp, reel->main_sound(), stage, progress, notes);
1198                                 }
1199
1200                                 if (reel->main_subtitle()) {
1201                                         verify_main_subtitle_reel (reel->main_subtitle(), notes);
1202                                         if (reel->main_subtitle()->asset_ref().resolved()) {
1203                                                 verify_subtitle_asset (reel->main_subtitle()->asset(), stage, xsd_dtd_directory, notes, state);
1204                                         }
1205                                         have_main_subtitle = true;
1206                                 } else {
1207                                         have_no_main_subtitle = true;
1208                                 }
1209
1210                                 for (auto i: reel->closed_captions()) {
1211                                         verify_closed_caption_reel (i, notes);
1212                                         if (i->asset_ref().resolved()) {
1213                                                 verify_closed_caption_asset (i->asset(), stage, xsd_dtd_directory, notes, state);
1214                                         }
1215                                 }
1216
1217                                 if (reel->main_markers()) {
1218                                         for (auto const& i: reel->main_markers()->get()) {
1219                                                 markers_seen.insert (i);
1220                                         }
1221                                 }
1222
1223                                 fewest_closed_captions = std::min (fewest_closed_captions, reel->closed_captions().size());
1224                                 most_closed_captions = std::max (most_closed_captions, reel->closed_captions().size());
1225                         }
1226
1227                         if (dcp->standard() == Standard::SMPTE) {
1228
1229                                 if (have_main_subtitle && have_no_main_subtitle) {
1230                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1231                                 }
1232
1233                                 if (fewest_closed_captions != most_closed_captions) {
1234                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1235                                 }
1236
1237                                 if (cpl->content_kind() == ContentKind::FEATURE) {
1238                                         if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1239                                                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1240                                         }
1241                                         if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1242                                                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1243                                         }
1244                                 }
1245
1246                                 auto ffoc = markers_seen.find(Marker::FFOC);
1247                                 if (ffoc == markers_seen.end()) {
1248                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1249                                 } else if (ffoc->second.e != 1) {
1250                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1251                                 }
1252
1253                                 auto lfoc = markers_seen.find(Marker::LFOC);
1254                                 if (lfoc == markers_seen.end()) {
1255                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1256                                 } else {
1257                                         auto lfoc_time = lfoc->second.as_editable_units(lfoc->second.tcr);
1258                                         if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1259                                                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1260                                         }
1261                                 }
1262
1263                                 verify_text_timing (cpl->reels(), notes);
1264
1265                                 LinesCharactersResult result;
1266                                 for (auto reel: cpl->reels()) {
1267                                         if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1268                                                 verify_text_lines_and_characters (reel->main_subtitle()->asset(), 52, 79, &result);
1269                                         }
1270                                 }
1271
1272                                 if (result.line_count_exceeded) {
1273                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1274                                 }
1275                                 if (result.error_length_exceeded) {
1276                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1277                                 } else if (result.warning_length_exceeded) {
1278                                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1279                                 }
1280
1281                                 result = LinesCharactersResult();
1282                                 for (auto reel: cpl->reels()) {
1283                                         for (auto i: reel->closed_captions()) {
1284                                                 if (i->asset()) {
1285                                                         verify_text_lines_and_characters (i->asset(), 32, 32, &result);
1286                                                 }
1287                                         }
1288                                 }
1289
1290                                 if (result.line_count_exceeded) {
1291                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1292                                 }
1293                                 if (result.error_length_exceeded) {
1294                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1295                                 }
1296
1297                                 if (!cpl->full_content_title_text()) {
1298                                         /* Since FullContentTitleText is assumed always to exist if there's a CompositionMetadataAsset we
1299                                          * can use it as a proxy for CompositionMetadataAsset's existence.
1300                                          */
1301                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1302                                 } else if (!cpl->version_number()) {
1303                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1304                                 }
1305
1306                                 verify_extension_metadata (cpl, notes);
1307
1308                                 if (cpl->any_encrypted()) {
1309                                         cxml::Document doc ("CompositionPlaylist");
1310                                         DCP_ASSERT (cpl->file());
1311                                         doc.read_file (cpl->file().get());
1312                                         if (!doc.optional_node_child("Signature")) {
1313                                                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1314                                         }
1315                                 }
1316                         }
1317                 }
1318
1319                 for (auto pkl: dcp->pkls()) {
1320                         stage ("Checking PKL", pkl->file());
1321                         validate_xml (pkl->file().get(), xsd_dtd_directory, notes);
1322                         if (pkl_has_encrypted_assets(dcp, pkl)) {
1323                                 cxml::Document doc ("PackingList");
1324                                 doc.read_file (pkl->file().get());
1325                                 if (!doc.optional_node_child("Signature")) {
1326                                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1327                                 }
1328                         }
1329                 }
1330
1331                 if (dcp->asset_map_path()) {
1332                         stage ("Checking ASSETMAP", dcp->asset_map_path().get());
1333                         validate_xml (dcp->asset_map_path().get(), xsd_dtd_directory, notes);
1334                 } else {
1335                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1336                 }
1337         }
1338
1339         return notes;
1340 }
1341
1342 string
1343 dcp::note_to_string (VerificationNote note)
1344 {
1345         /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1346          *
1347          *  e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1348          *  not "ClosedCaption assets must have an <EntryPoint> tag."
1349          *
1350          *  It's OK to use XML tag names where they are clear.
1351          *  If both ID and filename are available, use only the ID.
1352          *  End messages with a full stop.
1353          *  Messages should not mention whether or not their errors are a part of Bv2.1.
1354          */
1355         switch (note.code()) {
1356         case VerificationNote::Code::FAILED_READ:
1357                 return *note.note();
1358         case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1359                 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1360         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1361                 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1362         case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1363                 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1364         case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1365                 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1366         case VerificationNote::Code::INCORRECT_SOUND_HASH:
1367                 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1368         case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1369                 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1370         case VerificationNote::Code::EMPTY_ASSET_PATH:
1371                 return "The asset map contains an empty asset path.";
1372         case VerificationNote::Code::MISSING_ASSET:
1373                 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1374         case VerificationNote::Code::MISMATCHED_STANDARD:
1375                 return "The DCP contains both SMPTE and Interop parts.";
1376         case VerificationNote::Code::INVALID_XML:
1377                 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1378         case VerificationNote::Code::MISSING_ASSETMAP:
1379                 return "No ASSETMAP or ASSETMAP.xml was found.";
1380         case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1381                 return String::compose("The intrinsic duration of the asset %1 is less than 1 second long.", note.note().get());
1382         case VerificationNote::Code::INVALID_DURATION:
1383                 return String::compose("The duration of the asset %1 is less than 1 second long.", note.note().get());
1384         case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1385                 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());
1386         case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1387                 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());
1388         case VerificationNote::Code::EXTERNAL_ASSET:
1389                 return String::compose("The asset %1 that this DCP refers to is not included in the DCP.  It may be a VF.", note.note().get());
1390         case VerificationNote::Code::INVALID_STANDARD:
1391                 return "This DCP does not use the SMPTE standard.";
1392         case VerificationNote::Code::INVALID_LANGUAGE:
1393                 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1394         case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1395                 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1396         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1397                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1398         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1399                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1400         case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1401                 return "3D 4K DCPs are not allowed.";
1402         case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1403                 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1404         case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1405                 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1406         case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1407                 return String::compose("The size %1 of the fonts in timed text asset %2 is larger than the 10MB maximum.", note.note().get(), note.file()->filename());
1408         case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1409                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1410         case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1411                 return "Some subtitle assets have different <Language> tags than others";
1412         case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1413                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1414         case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1415                 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1416         case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1417                 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1418         case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1419                 return "At least one subtitle lasts less than 15 frames.";
1420         case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1421                 return "At least one pair of subtitles is separated by less than 2 frames.";
1422         case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1423                 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1424         case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1425                 return "There are more than 52 characters in at least one subtitle line.";
1426         case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1427                 return "There are more than 79 characters in at least one subtitle line.";
1428         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1429                 return "There are more than 3 closed caption lines in at least one place.";
1430         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1431                 return "There are more than 32 characters in at least one closed caption line.";
1432         case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1433                 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1434         case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1435                 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1436         case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1437                 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>", note.note().get());
1438         case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1439                 return "All assets in a reel do not have the same duration.";
1440         case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1441                 return "At least one reel contains a subtitle asset, but some reel(s) do not";
1442         case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1443                 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1444         case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1445                 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
1446         case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
1447                 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
1448         case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
1449                 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
1450         case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
1451                 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
1452         case VerificationNote::Code::MISSING_HASH:
1453                 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
1454         case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
1455                 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker";
1456         case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
1457                 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker";
1458         case VerificationNote::Code::MISSING_FFOC:
1459                 return "There should be a FFOC (first frame of content) marker";
1460         case VerificationNote::Code::MISSING_LFOC:
1461                 return "There should be a LFOC (last frame of content) marker";
1462         case VerificationNote::Code::INCORRECT_FFOC:
1463                 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
1464         case VerificationNote::Code::INCORRECT_LFOC:
1465                 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
1466         case VerificationNote::Code::MISSING_CPL_METADATA:
1467                 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
1468         case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
1469                 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
1470         case VerificationNote::Code::MISSING_EXTENSION_METADATA:
1471                 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
1472         case VerificationNote::Code::INVALID_EXTENSION_METADATA:
1473                 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
1474         case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
1475                 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
1476         case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
1477                 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
1478         case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
1479                 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>", note.note().get());
1480         case VerificationNote::Code::PARTIALLY_ENCRYPTED:
1481                 return "Some assets are encrypted but some are not";
1482         }
1483
1484         return "";
1485 }
1486
1487
1488 bool
1489 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
1490 {
1491         return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
1492 }
1493
1494 std::ostream&
1495 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
1496 {
1497         s << note_to_string (note);
1498         if (note.note()) {
1499                 s << " [" << note.note().get() << "]";
1500         }
1501         if (note.file()) {
1502                 s << " [" << note.file().get() << "]";
1503         }
1504         if (note.line()) {
1505                 s << " [" << note.line().get() << "]";
1506         }
1507         return s;
1508 }
1509