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