Allow use of KDMs when verifying DCPs.
[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 "filesystem.h"
45 #include "interop_subtitle_asset.h"
46 #include "mono_picture_asset.h"
47 #include "mono_picture_frame.h"
48 #include "raw_convert.h"
49 #include "reel.h"
50 #include "reel_closed_caption_asset.h"
51 #include "reel_interop_subtitle_asset.h"
52 #include "reel_markers_asset.h"
53 #include "reel_picture_asset.h"
54 #include "reel_sound_asset.h"
55 #include "reel_smpte_subtitle_asset.h"
56 #include "reel_subtitle_asset.h"
57 #include "smpte_subtitle_asset.h"
58 #include "stereo_picture_asset.h"
59 #include "stereo_picture_frame.h"
60 #include "verify.h"
61 #include "verify_j2k.h"
62 #include <libxml/parserInternals.h>
63 #include <xercesc/dom/DOMAttr.hpp>
64 #include <xercesc/dom/DOMDocument.hpp>
65 #include <xercesc/dom/DOMError.hpp>
66 #include <xercesc/dom/DOMErrorHandler.hpp>
67 #include <xercesc/dom/DOMException.hpp>
68 #include <xercesc/dom/DOMImplementation.hpp>
69 #include <xercesc/dom/DOMImplementationLS.hpp>
70 #include <xercesc/dom/DOMImplementationRegistry.hpp>
71 #include <xercesc/dom/DOMLSParser.hpp>
72 #include <xercesc/dom/DOMLocator.hpp>
73 #include <xercesc/dom/DOMNamedNodeMap.hpp>
74 #include <xercesc/dom/DOMNodeList.hpp>
75 #include <xercesc/framework/LocalFileInputSource.hpp>
76 #include <xercesc/framework/MemBufInputSource.hpp>
77 #include <xercesc/parsers/AbstractDOMParser.hpp>
78 #include <xercesc/parsers/XercesDOMParser.hpp>
79 #include <xercesc/sax/HandlerBase.hpp>
80 #include <xercesc/util/PlatformUtils.hpp>
81 #include <boost/algorithm/string.hpp>
82 #include <iostream>
83 #include <map>
84 #include <regex>
85 #include <set>
86 #include <vector>
87
88
89 using std::cout;
90 using std::dynamic_pointer_cast;
91 using std::list;
92 using std::make_shared;
93 using std::map;
94 using std::max;
95 using std::set;
96 using std::shared_ptr;
97 using std::string;
98 using std::vector;
99 using boost::optional;
100 using boost::function;
101
102
103 using namespace dcp;
104 using namespace xercesc;
105
106
107 static
108 string
109 xml_ch_to_string (XMLCh const * a)
110 {
111         char* x = XMLString::transcode(a);
112         string const o(x);
113         XMLString::release(&x);
114         return o;
115 }
116
117
118 class XMLValidationError
119 {
120 public:
121         XMLValidationError (SAXParseException const & e)
122                 : _message (xml_ch_to_string(e.getMessage()))
123                 , _line (e.getLineNumber())
124                 , _column (e.getColumnNumber())
125                 , _public_id (e.getPublicId() ? xml_ch_to_string(e.getPublicId()) : "")
126                 , _system_id (e.getSystemId() ? xml_ch_to_string(e.getSystemId()) : "")
127         {
128
129         }
130
131         string message () const {
132                 return _message;
133         }
134
135         uint64_t line () const {
136                 return _line;
137         }
138
139         uint64_t column () const {
140                 return _column;
141         }
142
143         string public_id () const {
144                 return _public_id;
145         }
146
147         string system_id () const {
148                 return _system_id;
149         }
150
151 private:
152         string _message;
153         uint64_t _line;
154         uint64_t _column;
155         string _public_id;
156         string _system_id;
157 };
158
159
160 class DCPErrorHandler : public ErrorHandler
161 {
162 public:
163         void warning(const SAXParseException& e) override
164         {
165                 maybe_add (XMLValidationError(e));
166         }
167
168         void error(const SAXParseException& e) override
169         {
170                 maybe_add (XMLValidationError(e));
171         }
172
173         void fatalError(const SAXParseException& e) override
174         {
175                 maybe_add (XMLValidationError(e));
176         }
177
178         void resetErrors() override {
179                 _errors.clear ();
180         }
181
182         list<XMLValidationError> errors () const {
183                 return _errors;
184         }
185
186 private:
187         void maybe_add (XMLValidationError e)
188         {
189                 /* XXX: nasty hack */
190                 if (
191                         e.message().find("schema document") != string::npos &&
192                         e.message().find("has different target namespace from the one specified in instance document") != string::npos
193                         ) {
194                         return;
195                 }
196
197                 _errors.push_back (e);
198         }
199
200         list<XMLValidationError> _errors;
201 };
202
203
204 class StringToXMLCh
205 {
206 public:
207         StringToXMLCh (string a)
208         {
209                 _buffer = XMLString::transcode(a.c_str());
210         }
211
212         StringToXMLCh (StringToXMLCh const&) = delete;
213         StringToXMLCh& operator= (StringToXMLCh const&) = delete;
214
215         ~StringToXMLCh ()
216         {
217                 XMLString::release (&_buffer);
218         }
219
220         XMLCh const * get () const {
221                 return _buffer;
222         }
223
224 private:
225         XMLCh* _buffer;
226 };
227
228
229 class LocalFileResolver : public EntityResolver
230 {
231 public:
232         LocalFileResolver (boost::filesystem::path xsd_dtd_directory)
233                 : _xsd_dtd_directory (xsd_dtd_directory)
234         {
235                 /* XXX: I'm not clear on what things need to be in this list; some XSDs are apparently, magically
236                  * found without being here.
237                  */
238                 add("http://www.w3.org/2001/XMLSchema.dtd", "XMLSchema.dtd");
239                 add("http://www.w3.org/2001/03/xml.xsd", "xml.xsd");
240                 add("http://www.w3.org/TR/2002/REC-xmldsig-core-20020212/xmldsig-core-schema.xsd", "xmldsig-core-schema.xsd");
241                 add("http://www.digicine.com/schemas/437-Y/2007/Main-Stereo-Picture-CPL.xsd", "Main-Stereo-Picture-CPL.xsd");
242                 add("http://www.digicine.com/PROTO-ASDCP-CPL-20040511.xsd", "PROTO-ASDCP-CPL-20040511.xsd");
243                 add("http://www.digicine.com/PROTO-ASDCP-PKL-20040311.xsd", "PROTO-ASDCP-PKL-20040311.xsd");
244                 add("http://www.digicine.com/PROTO-ASDCP-AM-20040311.xsd", "PROTO-ASDCP-AM-20040311.xsd");
245                 add("http://www.digicine.com/PROTO-ASDCP-CC-CPL-20070926#", "PROTO-ASDCP-CC-CPL-20070926.xsd");
246                 add("interop-subs", "DCSubtitle.v1.mattsson.xsd");
247                 add("http://www.smpte-ra.org/schemas/428-7/2010/DCST.xsd", "DCDMSubtitle-2010.xsd");
248                 add("http://www.smpte-ra.org/schemas/428-7/2014/DCST.xsd", "DCDMSubtitle-2014.xsd");
249                 add("http://www.smpte-ra.org/schemas/429-16/2014/CPL-Metadata", "SMPTE-429-16.xsd");
250                 add("http://www.dolby.com/schemas/2012/AD", "Dolby-2012-AD.xsd");
251                 add("http://www.smpte-ra.org/schemas/429-10/2008/Main-Stereo-Picture-CPL", "SMPTE-429-10-2008.xsd");
252         }
253
254         InputSource* resolveEntity(XMLCh const *, XMLCh const * system_id) override
255         {
256                 if (!system_id) {
257                         return 0;
258                 }
259                 auto system_id_str = xml_ch_to_string (system_id);
260                 auto p = _xsd_dtd_directory;
261                 if (_files.find(system_id_str) == _files.end()) {
262                         p /= system_id_str;
263                 } else {
264                         p /= _files[system_id_str];
265                 }
266                 StringToXMLCh ch (p.string());
267                 return new LocalFileInputSource(ch.get());
268         }
269
270 private:
271         void add (string uri, string file)
272         {
273                 _files[uri] = file;
274         }
275
276         std::map<string, string> _files;
277         boost::filesystem::path _xsd_dtd_directory;
278 };
279
280
281 static void
282 parse (XercesDOMParser& parser, boost::filesystem::path xml)
283 {
284         parser.parse(xml.c_str());
285 }
286
287
288 static void
289 parse (XercesDOMParser& parser, string xml)
290 {
291         xercesc::MemBufInputSource buf(reinterpret_cast<unsigned char const*>(xml.c_str()), xml.size(), "");
292         parser.parse(buf);
293 }
294
295
296 template <class T>
297 void
298 validate_xml (T xml, boost::filesystem::path xsd_dtd_directory, vector<VerificationNote>& notes)
299 {
300         try {
301                 XMLPlatformUtils::Initialize ();
302         } catch (XMLException& e) {
303                 throw MiscError ("Failed to initialise xerces library");
304         }
305
306         DCPErrorHandler error_handler;
307
308         /* All the xerces objects in this scope must be destroyed before XMLPlatformUtils::Terminate() is called */
309         {
310                 XercesDOMParser parser;
311                 parser.setValidationScheme(XercesDOMParser::Val_Always);
312                 parser.setDoNamespaces(true);
313                 parser.setDoSchema(true);
314
315                 vector<string> schema;
316                 schema.push_back("xml.xsd");
317                 schema.push_back("xmldsig-core-schema.xsd");
318                 schema.push_back("SMPTE-429-7-2006-CPL.xsd");
319                 schema.push_back("SMPTE-429-8-2006-PKL.xsd");
320                 schema.push_back("SMPTE-429-9-2007-AM.xsd");
321                 schema.push_back("Main-Stereo-Picture-CPL.xsd");
322                 schema.push_back("PROTO-ASDCP-CPL-20040511.xsd");
323                 schema.push_back("PROTO-ASDCP-PKL-20040311.xsd");
324                 schema.push_back("PROTO-ASDCP-AM-20040311.xsd");
325                 schema.push_back("DCSubtitle.v1.mattsson.xsd");
326                 schema.push_back("DCDMSubtitle-2010.xsd");
327                 schema.push_back("DCDMSubtitle-2014.xsd");
328                 schema.push_back("PROTO-ASDCP-CC-CPL-20070926.xsd");
329                 schema.push_back("SMPTE-429-16.xsd");
330                 schema.push_back("Dolby-2012-AD.xsd");
331                 schema.push_back("SMPTE-429-10-2008.xsd");
332                 schema.push_back("xlink.xsd");
333                 schema.push_back("SMPTE-335-2012.xsd");
334                 schema.push_back("SMPTE-395-2014-13-1-aaf.xsd");
335                 schema.push_back("isdcf-mca.xsd");
336                 schema.push_back("SMPTE-429-12-2008.xsd");
337
338                 /* XXX: I'm not especially clear what this is for, but it seems to be necessary.
339                  * Schemas that are not mentioned in this list are not read, and the things
340                  * they describe are not checked.
341                  */
342                 string locations;
343                 for (auto i: schema) {
344                         locations += String::compose("%1 %1 ", i, i);
345                 }
346
347                 parser.setExternalSchemaLocation(locations.c_str());
348                 parser.setValidationSchemaFullChecking(true);
349                 parser.setErrorHandler(&error_handler);
350
351                 LocalFileResolver resolver (xsd_dtd_directory);
352                 parser.setEntityResolver(&resolver);
353
354                 try {
355                         parser.resetDocumentPool();
356                         parse(parser, xml);
357                 } catch (XMLException& e) {
358                         throw MiscError(xml_ch_to_string(e.getMessage()));
359                 } catch (DOMException& e) {
360                         throw MiscError(xml_ch_to_string(e.getMessage()));
361                 } catch (...) {
362                         throw MiscError("Unknown exception from xerces");
363                 }
364         }
365
366         XMLPlatformUtils::Terminate ();
367
368         for (auto i: error_handler.errors()) {
369                 notes.push_back ({
370                         VerificationNote::Type::ERROR,
371                         VerificationNote::Code::INVALID_XML,
372                         i.message(),
373                         boost::trim_copy(i.public_id() + " " + i.system_id()),
374                         i.line()
375                 });
376         }
377 }
378
379
380 enum class VerifyAssetResult {
381         GOOD,
382         CPL_PKL_DIFFER,
383         BAD
384 };
385
386
387 static VerifyAssetResult
388 verify_asset (shared_ptr<const DCP> dcp, shared_ptr<const ReelFileAsset> reel_file_asset, function<void (float)> progress)
389 {
390         /* When reading the DCP the hash will have been set to the one from the PKL/CPL.
391          * We want to calculate the hash of the actual file contents here, so that we
392          * can check it.  unset_hash() means that this calculation will happen on the
393          * call to hash().
394          */
395         reel_file_asset->asset_ref()->unset_hash();
396         auto const actual_hash = reel_file_asset->asset_ref()->hash([progress](int64_t done, int64_t total) {
397                 progress(float(done) / total);
398         });
399
400         auto pkls = dcp->pkls();
401         /* We've read this DCP in so it must have at least one PKL */
402         DCP_ASSERT (!pkls.empty());
403
404         auto asset = reel_file_asset->asset_ref().asset();
405
406         optional<string> pkl_hash;
407         for (auto i: pkls) {
408                 pkl_hash = i->hash (reel_file_asset->asset_ref()->id());
409                 if (pkl_hash) {
410                         break;
411                 }
412         }
413
414         DCP_ASSERT (pkl_hash);
415
416         auto cpl_hash = reel_file_asset->hash();
417         if (cpl_hash && *cpl_hash != *pkl_hash) {
418                 return VerifyAssetResult::CPL_PKL_DIFFER;
419         }
420
421         if (actual_hash != *pkl_hash) {
422                 return VerifyAssetResult::BAD;
423         }
424
425         return VerifyAssetResult::GOOD;
426 }
427
428
429 void
430 verify_language_tag (string tag, vector<VerificationNote>& notes)
431 {
432         try {
433                 LanguageTag test (tag);
434         } catch (LanguageTagError &) {
435                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, tag});
436         }
437 }
438
439
440 static void
441 verify_picture_asset (shared_ptr<const ReelFileAsset> reel_file_asset, boost::filesystem::path file, vector<VerificationNote>& notes, function<void (float)> progress)
442 {
443         int biggest_frame = 0;
444         auto asset = dynamic_pointer_cast<PictureAsset>(reel_file_asset->asset_ref().asset());
445         auto const duration = asset->intrinsic_duration ();
446
447         auto check_and_add = [&notes](vector<VerificationNote> const& j2k_notes) {
448                 for (auto i: j2k_notes) {
449                         if (find(notes.begin(), notes.end(), i) == notes.end()) {
450                                 notes.push_back (i);
451                         }
452                 }
453         };
454
455         if (auto mono_asset = dynamic_pointer_cast<MonoPictureAsset>(reel_file_asset->asset_ref().asset())) {
456                 auto reader = mono_asset->start_read ();
457                 for (int64_t i = 0; i < duration; ++i) {
458                         auto frame = reader->get_frame (i);
459                         biggest_frame = max(biggest_frame, frame->size());
460                         if (!mono_asset->encrypted() || mono_asset->key()) {
461                                 vector<VerificationNote> j2k_notes;
462                                 verify_j2k(frame, i, mono_asset->frame_rate().numerator, j2k_notes);
463                                 check_and_add (j2k_notes);
464                         }
465                         progress (float(i) / duration);
466                 }
467         } else if (auto stereo_asset = dynamic_pointer_cast<StereoPictureAsset>(asset)) {
468                 auto reader = stereo_asset->start_read ();
469                 for (int64_t i = 0; i < duration; ++i) {
470                         auto frame = reader->get_frame (i);
471                         biggest_frame = max(biggest_frame, max(frame->left()->size(), frame->right()->size()));
472                         if (!stereo_asset->encrypted() || stereo_asset->key()) {
473                                 vector<VerificationNote> j2k_notes;
474                                 verify_j2k(frame->left(), i, stereo_asset->frame_rate().numerator, j2k_notes);
475                                 verify_j2k(frame->right(), i, stereo_asset->frame_rate().numerator, j2k_notes);
476                                 check_and_add (j2k_notes);
477                         }
478                         progress (float(i) / duration);
479                 }
480
481         }
482
483         static const int max_frame =   rint(250 * 1000000 / (8 * asset->edit_rate().as_float()));
484         static const int risky_frame = rint(230 * 1000000 / (8 * asset->edit_rate().as_float()));
485         if (biggest_frame > max_frame) {
486                 notes.push_back ({
487                         VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
488                 });
489         } else if (biggest_frame > risky_frame) {
490                 notes.push_back ({
491                         VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, file
492                 });
493         }
494 }
495
496
497 static void
498 verify_main_picture_asset (
499         shared_ptr<const DCP> dcp,
500         shared_ptr<const ReelPictureAsset> reel_asset,
501         function<void (string, optional<boost::filesystem::path>)> stage,
502         function<void (float)> progress,
503         VerificationOptions options,
504         vector<VerificationNote>& notes
505         )
506 {
507         auto asset = reel_asset->asset();
508         auto const file = *asset->file();
509
510         if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
511                 stage ("Checking picture asset hash", file);
512                 auto const r = verify_asset (dcp, reel_asset, progress);
513                 switch (r) {
514                         case VerifyAssetResult::BAD:
515                                 notes.push_back ({
516                                         VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_PICTURE_HASH, file
517                                 });
518                                 break;
519                         case VerifyAssetResult::CPL_PKL_DIFFER:
520                                 notes.push_back ({
521                                         VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_PICTURE_HASHES, file
522                                 });
523                                 break;
524                         default:
525                                 break;
526                 }
527         }
528
529         stage ("Checking picture frame sizes", asset->file());
530         verify_picture_asset (reel_asset, file, notes, progress);
531
532         /* Only flat/scope allowed by Bv2.1 */
533         if (
534                 asset->size() != Size(2048, 858) &&
535                 asset->size() != Size(1998, 1080) &&
536                 asset->size() != Size(4096, 1716) &&
537                 asset->size() != Size(3996, 2160)) {
538                 notes.push_back({
539                         VerificationNote::Type::BV21_ERROR,
540                         VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS,
541                         String::compose("%1x%2", asset->size().width, asset->size().height),
542                         file
543                 });
544         }
545
546         /* Only 24, 25, 48fps allowed for 2K */
547         if (
548                 (asset->size() == Size(2048, 858) || asset->size() == Size(1998, 1080)) &&
549                 (asset->edit_rate() != Fraction(24, 1) && asset->edit_rate() != Fraction(25, 1) && asset->edit_rate() != Fraction(48, 1))
550            ) {
551                 notes.push_back({
552                         VerificationNote::Type::BV21_ERROR,
553                         VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K,
554                         String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
555                         file
556                 });
557         }
558
559         if (asset->size() == Size(4096, 1716) || asset->size() == Size(3996, 2160)) {
560                 /* Only 24fps allowed for 4K */
561                 if (asset->edit_rate() != Fraction(24, 1)) {
562                         notes.push_back({
563                                 VerificationNote::Type::BV21_ERROR,
564                                 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K,
565                                 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
566                                 file
567                         });
568                 }
569
570                 /* Only 2D allowed for 4K */
571                 if (dynamic_pointer_cast<const StereoPictureAsset>(asset)) {
572                         notes.push_back({
573                                 VerificationNote::Type::BV21_ERROR,
574                                 VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D,
575                                 String::compose("%1/%2", asset->edit_rate().numerator, asset->edit_rate().denominator),
576                                 file
577                         });
578
579                 }
580         }
581
582 }
583
584
585 struct State
586 {
587         boost::optional<string> subtitle_language;
588         boost::optional<int> audio_channels;
589 };
590
591
592 static void
593 verify_main_sound_asset (
594         shared_ptr<const DCP> dcp,
595         shared_ptr<const ReelSoundAsset> reel_asset,
596         function<void (string, optional<boost::filesystem::path>)> stage,
597         function<void (float)> progress,
598         VerificationOptions options,
599         vector<VerificationNote>& notes,
600         State& state
601         )
602 {
603         auto asset = reel_asset->asset();
604         auto const file = *asset->file();
605
606         if (options.check_asset_hashes && (!options.maximum_asset_size_for_hash_check || filesystem::file_size(file) < *options.maximum_asset_size_for_hash_check)) {
607                 stage("Checking sound asset hash", file);
608                 auto const r = verify_asset (dcp, reel_asset, progress);
609                 switch (r) {
610                         case VerifyAssetResult::BAD:
611                                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_SOUND_HASH, file});
612                                 break;
613                         case VerifyAssetResult::CPL_PKL_DIFFER:
614                                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_HASHES, file});
615                                 break;
616                         default:
617                                 break;
618                 }
619         }
620
621         if (!state.audio_channels) {
622                 state.audio_channels = asset->channels();
623         } else if (*state.audio_channels != asset->channels()) {
624                 notes.push_back({ VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, file });
625         }
626
627         stage ("Checking sound asset metadata", file);
628
629         if (auto lang = asset->language()) {
630                 verify_language_tag (*lang, notes);
631         }
632         if (asset->sampling_rate() != 48000) {
633                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SOUND_FRAME_RATE, raw_convert<string>(asset->sampling_rate()), file});
634         }
635 }
636
637
638 static void
639 verify_main_subtitle_reel (shared_ptr<const ReelSubtitleAsset> reel_asset, vector<VerificationNote>& notes)
640 {
641         /* XXX: is Language compulsory? */
642         if (reel_asset->language()) {
643                 verify_language_tag (*reel_asset->language(), notes);
644         }
645
646         if (!reel_asset->entry_point()) {
647                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT, reel_asset->id() });
648         } else if (reel_asset->entry_point().get()) {
649                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT, reel_asset->id() });
650         }
651 }
652
653
654 static void
655 verify_closed_caption_reel (shared_ptr<const ReelClosedCaptionAsset> reel_asset, vector<VerificationNote>& notes)
656 {
657         /* XXX: is Language compulsory? */
658         if (reel_asset->language()) {
659                 verify_language_tag (*reel_asset->language(), notes);
660         }
661
662         if (!reel_asset->entry_point()) {
663                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
664         } else if (reel_asset->entry_point().get()) {
665                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT, reel_asset->id() });
666         }
667 }
668
669
670 /** Verify stuff that is common to both subtitles and closed captions */
671 void
672 verify_smpte_timed_text_asset (
673         shared_ptr<const SMPTESubtitleAsset> asset,
674         optional<int64_t> reel_asset_duration,
675         vector<VerificationNote>& notes
676         )
677 {
678         if (asset->language()) {
679                 verify_language_tag (*asset->language(), notes);
680         } else {
681                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, *asset->file() });
682         }
683
684         auto const size = filesystem::file_size(asset->file().get());
685         if (size > 115 * 1024 * 1024) {
686                 notes.push_back (
687                         { VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, raw_convert<string>(size), *asset->file() }
688                         );
689         }
690
691         /* XXX: I'm not sure what Bv2.1_7.2.1 means when it says "the font resource shall not be larger than 10MB"
692          * but I'm hoping that checking for the total size of all fonts being <= 10MB will do.
693          */
694         auto fonts = asset->font_data ();
695         int total_size = 0;
696         for (auto i: fonts) {
697                 total_size += i.second.size();
698         }
699         if (total_size > 10 * 1024 * 1024) {
700                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, raw_convert<string>(total_size), asset->file().get() });
701         }
702
703         if (!asset->start_time()) {
704                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_SUBTITLE_START_TIME, asset->file().get() });
705         } else if (asset->start_time() != Time()) {
706                 notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_SUBTITLE_START_TIME, asset->file().get() });
707         }
708
709         if (reel_asset_duration && *reel_asset_duration != asset->intrinsic_duration()) {
710                 notes.push_back (
711                         {
712                                 VerificationNote::Type::BV21_ERROR,
713                                 VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION,
714                                 String::compose("%1 %2", *reel_asset_duration, asset->intrinsic_duration()),
715                                 asset->file().get()
716                         });
717         }
718 }
719
720
721 /** Verify Interop subtitle / CCAP stuff */
722 void
723 verify_interop_text_asset(shared_ptr<const InteropSubtitleAsset> asset, vector<VerificationNote>& notes)
724 {
725         if (asset->subtitles().empty()) {
726                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_SUBTITLE, asset->id(), asset->file().get() });
727         }
728         auto const unresolved = asset->unresolved_fonts();
729         if (!unresolved.empty()) {
730                 notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_FONT, unresolved.front() });
731         }
732 }
733
734
735 /** Verify SMPTE subtitle-only stuff */
736 void
737 verify_smpte_subtitle_asset (
738         shared_ptr<const SMPTESubtitleAsset> asset,
739         vector<VerificationNote>& notes,
740         State& state
741         )
742 {
743         if (asset->language()) {
744                 if (!state.subtitle_language) {
745                         state.subtitle_language = *asset->language();
746                 } else if (state.subtitle_language != *asset->language()) {
747                         notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES });
748                 }
749         }
750
751         DCP_ASSERT (asset->resource_id());
752         auto xml_id = asset->xml_id();
753         if (xml_id) {
754                 if (asset->resource_id().get() != xml_id) {
755                         notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID });
756                 }
757
758                 if (asset->id() == asset->resource_id().get() || asset->id() == xml_id) {
759                         notes.push_back ({ VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID });
760                 }
761         } else {
762                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
763         }
764
765         if (asset->raw_xml()) {
766                 /* Deluxe require this in their QC even if it seems never to be mentioned in any standard */
767                 cxml::Document doc("SubtitleReel");
768                 doc.read_string(*asset->raw_xml());
769                 auto issue_date = doc.string_child("IssueDate");
770                 std::regex reg("^\\d\\d\\d\\d-\\d\\d-\\d\\dT\\d\\d:\\d\\d:\\d\\d$");
771                 if (!std::regex_match(issue_date, reg)) {
772                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, issue_date});
773                 }
774         }
775 }
776
777
778 /** Verify all subtitle stuff */
779 static void
780 verify_subtitle_asset (
781         shared_ptr<const SubtitleAsset> asset,
782         optional<int64_t> reel_asset_duration,
783         function<void (string, optional<boost::filesystem::path>)> stage,
784         boost::filesystem::path xsd_dtd_directory,
785         vector<VerificationNote>& notes,
786         State& state
787         )
788 {
789         stage ("Checking subtitle XML", asset->file());
790         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
791          * gets passed through libdcp which may clean up and therefore hide errors.
792          */
793         if (asset->raw_xml()) {
794                 validate_xml (asset->raw_xml().get(), xsd_dtd_directory, notes);
795         } else {
796                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
797         }
798
799         auto namespace_count = [](shared_ptr<const SubtitleAsset> asset, string root_node) {
800                 cxml::Document doc(root_node);
801                 doc.read_string(asset->raw_xml().get());
802                 auto root = dynamic_cast<xmlpp::Element*>(doc.node())->cobj();
803                 int count = 0;
804                 for (auto ns = root->nsDef; ns != nullptr; ns = ns->next) {
805                         ++count;
806                 }
807                 return count;
808         };
809
810         auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
811         if (interop) {
812                 verify_interop_text_asset(interop, notes);
813                 if (namespace_count(asset, "DCSubtitle") > 1) {
814                         notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id() });
815                 }
816         }
817
818         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
819         if (smpte) {
820                 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
821                 verify_smpte_subtitle_asset (smpte, notes, state);
822                 /* This asset may be encrypted and in that case we'll have no raw_xml() */
823                 if (asset->raw_xml() && namespace_count(asset, "SubtitleReel") > 1) {
824                         notes.push_back({ VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, asset->id()});
825                 }
826         }
827 }
828
829
830 /** Verify all closed caption stuff */
831 static void
832 verify_closed_caption_asset (
833         shared_ptr<const SubtitleAsset> asset,
834         optional<int64_t> reel_asset_duration,
835         function<void (string, optional<boost::filesystem::path>)> stage,
836         boost::filesystem::path xsd_dtd_directory,
837         vector<VerificationNote>& notes
838         )
839 {
840         stage ("Checking closed caption XML", asset->file());
841         /* Note: we must not use SubtitleAsset::xml_as_string() here as that will mean the data on disk
842          * gets passed through libdcp which may clean up and therefore hide errors.
843          */
844         auto raw_xml = asset->raw_xml();
845         if (raw_xml) {
846                 validate_xml (*raw_xml, xsd_dtd_directory, notes);
847                 if (raw_xml->size() > 256 * 1024) {
848                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES, raw_convert<string>(raw_xml->size()), *asset->file()});
849                 }
850         } else {
851                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
852         }
853
854         auto interop = dynamic_pointer_cast<const InteropSubtitleAsset>(asset);
855         if (interop) {
856                 verify_interop_text_asset(interop, notes);
857         }
858
859         auto smpte = dynamic_pointer_cast<const SMPTESubtitleAsset>(asset);
860         if (smpte) {
861                 verify_smpte_timed_text_asset (smpte, reel_asset_duration, notes);
862         }
863 }
864
865
866 /** Check the timing of the individual subtitles and make sure there are no empty <Text> nodes etc. */
867 static
868 void
869 verify_text_details (
870         dcp::Standard standard,
871         vector<shared_ptr<Reel>> reels,
872         int edit_rate,
873         vector<VerificationNote>& notes,
874         std::function<bool (shared_ptr<Reel>)> check,
875         std::function<optional<string> (shared_ptr<Reel>)> xml,
876         std::function<int64_t (shared_ptr<Reel>)> duration,
877         std::function<std::string (shared_ptr<Reel>)> id
878         )
879 {
880         /* end of last subtitle (in editable units) */
881         optional<int64_t> last_out;
882         auto too_short = false;
883         auto too_close = false;
884         auto too_early = false;
885         auto reel_overlap = false;
886         auto empty_text = false;
887         /* current reel start time (in editable units) */
888         int64_t reel_offset = 0;
889         optional<string> missing_load_font_id;
890
891         std::function<void (cxml::ConstNodePtr, optional<int>, optional<Time>, int, bool, bool&, vector<string>&)> parse;
892
893         parse = [&parse, &last_out, &too_short, &too_close, &too_early, &empty_text, &reel_offset, &missing_load_font_id](
894                 cxml::ConstNodePtr node,
895                 optional<int> tcr,
896                 optional<Time> start_time,
897                 int er,
898                 bool first_reel,
899                 bool& has_text,
900                 vector<string>& font_ids
901                 ) {
902                 if (node->name() == "Subtitle") {
903                         Time in (node->string_attribute("TimeIn"), tcr);
904                         if (start_time) {
905                                 in -= *start_time;
906                         }
907                         Time out (node->string_attribute("TimeOut"), tcr);
908                         if (start_time) {
909                                 out -= *start_time;
910                         }
911                         if (first_reel && tcr && in < Time(0, 0, 4, 0, *tcr)) {
912                                 too_early = true;
913                         }
914                         auto length = out - in;
915                         if (length.as_editable_units_ceil(er) < 15) {
916                                 too_short = true;
917                         }
918                         if (last_out) {
919                                 /* XXX: this feels dubious - is it really what Bv2.1 means? */
920                                 auto distance = reel_offset + in.as_editable_units_ceil(er) - *last_out;
921                                 if (distance >= 0 && distance < 2) {
922                                         too_close = true;
923                                 }
924                         }
925                         last_out = reel_offset + out.as_editable_units_floor(er);
926                 } else if (node->name() == "Text") {
927                         std::function<bool (cxml::ConstNodePtr)> node_has_content = [&](cxml::ConstNodePtr node) {
928                                 if (!node->content().empty()) {
929                                         return true;
930                                 }
931                                 for (auto i: node->node_children()) {
932                                         if (node_has_content(i)) {
933                                                 return true;
934                                         }
935                                 }
936                                 return false;
937                         };
938                         if (!node_has_content(node)) {
939                                 empty_text = true;
940                         }
941                         has_text = true;
942                 } else if (node->name() == "LoadFont") {
943                         if (auto const id = node->optional_string_attribute("Id")) {
944                                 font_ids.push_back(*id);
945                         } else if (auto const id = node->optional_string_attribute("ID")) {
946                                 font_ids.push_back(*id);
947                         }
948                 } else if (node->name() == "Font") {
949                         if (auto const font_id = node->optional_string_attribute("Id")) {
950                                 if (std::find_if(font_ids.begin(), font_ids.end(), [font_id](string const& id) { return id == font_id; }) == font_ids.end()) {
951                                         missing_load_font_id = font_id;
952                                 }
953                         }
954                 }
955                 for (auto i: node->node_children()) {
956                         parse(i, tcr, start_time, er, first_reel, has_text, font_ids);
957                 }
958         };
959
960         for (auto i = 0U; i < reels.size(); ++i) {
961                 if (!check(reels[i])) {
962                         continue;
963                 }
964
965                 auto reel_xml = xml(reels[i]);
966                 if (!reel_xml) {
967                         notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
968                         continue;
969                 }
970
971                 /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
972                  * read in by libdcp's parser.
973                  */
974
975                 shared_ptr<cxml::Document> doc;
976                 optional<int> tcr;
977                 optional<Time> start_time;
978                 switch (standard) {
979                 case dcp::Standard::INTEROP:
980                         doc = make_shared<cxml::Document>("DCSubtitle");
981                         doc->read_string (*reel_xml);
982                         break;
983                 case dcp::Standard::SMPTE:
984                         doc = make_shared<cxml::Document>("SubtitleReel");
985                         doc->read_string (*reel_xml);
986                         tcr = doc->number_child<int>("TimeCodeRate");
987                         if (auto start_time_string = doc->optional_string_child("StartTime")) {
988                                 start_time = Time(*start_time_string, tcr);
989                         }
990                         break;
991                 }
992                 bool has_text = false;
993                 vector<string> font_ids;
994                 parse(doc, tcr, start_time, edit_rate, i == 0, has_text, font_ids);
995                 auto end = reel_offset + duration(reels[i]);
996                 if (last_out && *last_out > end) {
997                         reel_overlap = true;
998                 }
999                 reel_offset = end;
1000
1001                 if (standard == dcp::Standard::SMPTE && has_text && font_ids.empty()) {
1002                         notes.push_back(dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT).set_id(id(reels[i])));
1003                 }
1004         }
1005
1006         if (last_out && *last_out > reel_offset) {
1007                 reel_overlap = true;
1008         }
1009
1010         if (too_early) {
1011                 notes.push_back({
1012                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME
1013                 });
1014         }
1015
1016         if (too_short) {
1017                 notes.push_back ({
1018                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_DURATION
1019                 });
1020         }
1021
1022         if (too_close) {
1023                 notes.push_back ({
1024                         VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_SPACING
1025                 });
1026         }
1027
1028         if (reel_overlap) {
1029                 notes.push_back ({
1030                         VerificationNote::Type::ERROR, VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY
1031                 });
1032         }
1033
1034         if (empty_text) {
1035                 notes.push_back ({
1036                         VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_TEXT
1037                 });
1038         }
1039
1040         if (missing_load_font_id) {
1041                 notes.push_back(dcp::VerificationNote(VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id(*missing_load_font_id));
1042         }
1043 }
1044
1045
1046 static
1047 void
1048 verify_closed_caption_details (
1049         vector<shared_ptr<Reel>> reels,
1050         vector<VerificationNote>& notes
1051         )
1052 {
1053         std::function<void (cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image)> find_text_or_image;
1054         find_text_or_image = [&find_text_or_image](cxml::ConstNodePtr node, std::vector<cxml::ConstNodePtr>& text_or_image) {
1055                 for (auto i: node->node_children()) {
1056                         if (i->name() == "Text") {
1057                                 text_or_image.push_back (i);
1058                         } else {
1059                                 find_text_or_image (i, text_or_image);
1060                         }
1061                 }
1062         };
1063
1064         auto mismatched_valign = false;
1065         auto incorrect_order = false;
1066
1067         std::function<void (cxml::ConstNodePtr)> parse;
1068         parse = [&parse, &find_text_or_image, &mismatched_valign, &incorrect_order](cxml::ConstNodePtr node) {
1069                 if (node->name() == "Subtitle") {
1070                         vector<cxml::ConstNodePtr> text_or_image;
1071                         find_text_or_image (node, text_or_image);
1072                         optional<string> last_valign;
1073                         optional<float> last_vpos;
1074                         for (auto i: text_or_image) {
1075                                 auto valign = i->optional_string_attribute("VAlign");
1076                                 if (!valign) {
1077                                         valign = i->optional_string_attribute("Valign").get_value_or("center");
1078                                 }
1079                                 auto vpos = i->optional_number_attribute<float>("VPosition");
1080                                 if (!vpos) {
1081                                         vpos = i->optional_number_attribute<float>("Vposition").get_value_or(50);
1082                                 }
1083
1084                                 if (last_valign) {
1085                                         if (*last_valign != valign) {
1086                                                 mismatched_valign = true;
1087                                         }
1088                                 }
1089                                 last_valign = valign;
1090
1091                                 if (!mismatched_valign) {
1092                                         if (last_vpos) {
1093                                                 if (*last_valign == "top" || *last_valign == "center") {
1094                                                         if (*vpos < *last_vpos) {
1095                                                                 incorrect_order = true;
1096                                                         }
1097                                                 } else {
1098                                                         if (*vpos > *last_vpos) {
1099                                                                 incorrect_order = true;
1100                                                         }
1101                                                 }
1102                                         }
1103                                         last_vpos = vpos;
1104                                 }
1105                         }
1106                 }
1107
1108                 for (auto i: node->node_children()) {
1109                         parse(i);
1110                 }
1111         };
1112
1113         for (auto reel: reels) {
1114                 for (auto ccap: reel->closed_captions()) {
1115                         auto reel_xml = ccap->asset()->raw_xml();
1116                         if (!reel_xml) {
1117                                 notes.push_back ({VerificationNote::Type::WARNING, VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED});
1118                                 continue;
1119                         }
1120
1121                         /* We need to look at <Subtitle> instances in the XML being checked, so we can't use the subtitles
1122                          * read in by libdcp's parser.
1123                          */
1124
1125                         shared_ptr<cxml::Document> doc;
1126                         optional<int> tcr;
1127                         optional<Time> start_time;
1128                         try {
1129                                 doc = make_shared<cxml::Document>("SubtitleReel");
1130                                 doc->read_string (*reel_xml);
1131                         } catch (...) {
1132                                 doc = make_shared<cxml::Document>("DCSubtitle");
1133                                 doc->read_string (*reel_xml);
1134                         }
1135                         parse (doc);
1136                 }
1137         }
1138
1139         if (mismatched_valign) {
1140                 notes.push_back ({
1141                         VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN,
1142                 });
1143         }
1144
1145         if (incorrect_order) {
1146                 notes.push_back ({
1147                         VerificationNote::Type::ERROR, VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING,
1148                 });
1149         }
1150 }
1151
1152
1153 struct LinesCharactersResult
1154 {
1155         bool warning_length_exceeded = false;
1156         bool error_length_exceeded = false;
1157         bool line_count_exceeded = false;
1158 };
1159
1160
1161 static
1162 void
1163 verify_text_lines_and_characters (
1164         shared_ptr<SubtitleAsset> asset,
1165         int warning_length,
1166         int error_length,
1167         LinesCharactersResult* result
1168         )
1169 {
1170         class Event
1171         {
1172         public:
1173                 Event (Time time_, float position_, int characters_)
1174                         : time (time_)
1175                         , position (position_)
1176                         , characters (characters_)
1177                 {}
1178
1179                 Event (Time time_, shared_ptr<Event> start_)
1180                         : time (time_)
1181                         , start (start_)
1182                 {}
1183
1184                 Time time;
1185                 int position; //< position from 0 at top of screen to 100 at bottom
1186                 int characters;
1187                 shared_ptr<Event> start;
1188         };
1189
1190         vector<shared_ptr<Event>> events;
1191
1192         auto position = [](shared_ptr<const SubtitleString> sub) {
1193                 switch (sub->v_align()) {
1194                 case VAlign::TOP:
1195                         return lrintf(sub->v_position() * 100);
1196                 case VAlign::CENTER:
1197                         return lrintf((0.5f + sub->v_position()) * 100);
1198                 case VAlign::BOTTOM:
1199                         return lrintf((1.0f - sub->v_position()) * 100);
1200                 }
1201
1202                 return 0L;
1203         };
1204
1205         for (auto j: asset->subtitles()) {
1206                 auto text = dynamic_pointer_cast<const SubtitleString>(j);
1207                 if (text) {
1208                         auto in = make_shared<Event>(text->in(), position(text), text->text().length());
1209                         events.push_back(in);
1210                         events.push_back(make_shared<Event>(text->out(), in));
1211                 }
1212         }
1213
1214         std::sort(events.begin(), events.end(), [](shared_ptr<Event> const& a, shared_ptr<Event>const& b) {
1215                 return a->time < b->time;
1216         });
1217
1218         map<int, int> current;
1219         for (auto i: events) {
1220                 if (current.size() > 3) {
1221                         result->line_count_exceeded = true;
1222                 }
1223                 for (auto j: current) {
1224                         if (j.second > warning_length) {
1225                                 result->warning_length_exceeded = true;
1226                         }
1227                         if (j.second > error_length) {
1228                                 result->error_length_exceeded = true;
1229                         }
1230                 }
1231
1232                 if (i->start) {
1233                         /* end of a subtitle */
1234                         DCP_ASSERT (current.find(i->start->position) != current.end());
1235                         if (current[i->start->position] == i->start->characters) {
1236                                 current.erase(i->start->position);
1237                         } else {
1238                                 current[i->start->position] -= i->start->characters;
1239                         }
1240                 } else {
1241                         /* start of a subtitle */
1242                         if (current.find(i->position) == current.end()) {
1243                                 current[i->position] = i->characters;
1244                         } else {
1245                                 current[i->position] += i->characters;
1246                         }
1247                 }
1248         }
1249 }
1250
1251
1252 static
1253 void
1254 verify_text_details(dcp::Standard standard, vector<shared_ptr<Reel>> reels, vector<VerificationNote>& notes)
1255 {
1256         if (reels.empty()) {
1257                 return;
1258         }
1259
1260         if (reels[0]->main_subtitle()) {
1261                 verify_text_details(standard, reels, reels[0]->main_subtitle()->edit_rate().numerator, notes,
1262                         [](shared_ptr<Reel> reel) {
1263                                 return static_cast<bool>(reel->main_subtitle());
1264                         },
1265                         [](shared_ptr<Reel> reel) {
1266                                 return reel->main_subtitle()->asset()->raw_xml();
1267                         },
1268                         [](shared_ptr<Reel> reel) {
1269                                 return reel->main_subtitle()->actual_duration();
1270                         },
1271                         [](shared_ptr<Reel> reel) {
1272                                 return reel->main_subtitle()->id();
1273                         }
1274                 );
1275         }
1276
1277         for (auto i = 0U; i < reels[0]->closed_captions().size(); ++i) {
1278                 verify_text_details(standard, reels, reels[0]->closed_captions()[i]->edit_rate().numerator, notes,
1279                         [i](shared_ptr<Reel> reel) {
1280                                 return i < reel->closed_captions().size();
1281                         },
1282                         [i](shared_ptr<Reel> reel) {
1283                                 return reel->closed_captions()[i]->asset()->raw_xml();
1284                         },
1285                         [i](shared_ptr<Reel> reel) {
1286                                 return reel->closed_captions()[i]->actual_duration();
1287                         },
1288                         [i](shared_ptr<Reel> reel) {
1289                                 return reel->closed_captions()[i]->id();
1290                         }
1291                 );
1292         }
1293
1294         verify_closed_caption_details (reels, notes);
1295 }
1296
1297
1298 void
1299 verify_extension_metadata(shared_ptr<const CPL> cpl, vector<VerificationNote>& notes)
1300 {
1301         DCP_ASSERT (cpl->file());
1302         cxml::Document doc ("CompositionPlaylist");
1303         doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1304
1305         auto missing = false;
1306         string malformed;
1307
1308         if (auto reel_list = doc.node_child("ReelList")) {
1309                 auto reels = reel_list->node_children("Reel");
1310                 if (!reels.empty()) {
1311                         if (auto asset_list = reels[0]->optional_node_child("AssetList")) {
1312                                 if (auto metadata = asset_list->optional_node_child("CompositionMetadataAsset")) {
1313                                         if (auto extension_list = metadata->optional_node_child("ExtensionMetadataList")) {
1314                                                 missing = true;
1315                                                 for (auto extension: extension_list->node_children("ExtensionMetadata")) {
1316                                                         if (extension->optional_string_attribute("scope").get_value_or("") != "http://isdcf.com/ns/cplmd/app") {
1317                                                                 continue;
1318                                                         }
1319                                                         missing = false;
1320                                                         if (auto name = extension->optional_node_child("Name")) {
1321                                                                 if (name->content() != "Application") {
1322                                                                         malformed = "<Name> should be 'Application'";
1323                                                                 }
1324                                                         }
1325                                                         if (auto property_list = extension->optional_node_child("PropertyList")) {
1326                                                                 if (auto property = property_list->optional_node_child("Property")) {
1327                                                                         if (auto name = property->optional_node_child("Name")) {
1328                                                                                 if (name->content() != "DCP Constraints Profile") {
1329                                                                                         malformed = "<Name> property should be 'DCP Constraints Profile'";
1330                                                                                 }
1331                                                                         }
1332                                                                         if (auto value = property->optional_node_child("Value")) {
1333                                                                                 if (value->content() != "SMPTE-RDD-52:2020-Bv2.1") {
1334                                                                                         malformed = "<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'";
1335                                                                                 }
1336                                                                         }
1337                                                                 }
1338                                                         }
1339                                                 }
1340                                         } else {
1341                                                 missing = true;
1342                                         }
1343                                 }
1344                         }
1345                 }
1346         }
1347
1348         if (missing) {
1349                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get()});
1350         } else if (!malformed.empty()) {
1351                 notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_EXTENSION_METADATA, malformed, cpl->file().get()});
1352         }
1353 }
1354
1355
1356 bool
1357 pkl_has_encrypted_assets(shared_ptr<const DCP> dcp, shared_ptr<const PKL> pkl)
1358 {
1359         vector<string> encrypted;
1360         for (auto i: dcp->cpls()) {
1361                 for (auto j: i->reel_file_assets()) {
1362                         if (j->asset_ref().resolved()) {
1363                                 auto mxf = dynamic_pointer_cast<MXF>(j->asset_ref().asset());
1364                                 if (mxf && mxf->encrypted()) {
1365                                         encrypted.push_back(j->asset_ref().id());
1366                                 }
1367                         }
1368                 }
1369         }
1370
1371         for (auto i: pkl->assets()) {
1372                 if (find(encrypted.begin(), encrypted.end(), i->id()) != encrypted.end()) {
1373                         return true;
1374                 }
1375         }
1376
1377         return false;
1378 }
1379
1380
1381 static
1382 void
1383 verify_reel(
1384         shared_ptr<const DCP> dcp,
1385         shared_ptr<const CPL> cpl,
1386         shared_ptr<const Reel> reel,
1387         optional<dcp::Size> main_picture_active_area,
1388         function<void (string, optional<boost::filesystem::path>)> stage,
1389         boost::filesystem::path xsd_dtd_directory,
1390         function<void (float)> progress,
1391         VerificationOptions options,
1392         vector<VerificationNote>& notes,
1393         State& state,
1394         bool* have_main_subtitle,
1395         bool* have_no_main_subtitle,
1396         size_t* most_closed_captions,
1397         size_t* fewest_closed_captions,
1398         map<Marker, Time>* markers_seen
1399         )
1400 {
1401         for (auto i: reel->assets()) {
1402                 if (i->duration() && (i->duration().get() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1403                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_DURATION, i->id()});
1404                 }
1405                 if ((i->intrinsic_duration() * i->edit_rate().denominator / i->edit_rate().numerator) < 1) {
1406                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_INTRINSIC_DURATION, i->id()});
1407                 }
1408                 auto file_asset = dynamic_pointer_cast<ReelFileAsset>(i);
1409                 if (i->encryptable() && !file_asset->hash()) {
1410                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_HASH, i->id()});
1411                 }
1412         }
1413
1414         if (dcp->standard() == Standard::SMPTE) {
1415                 boost::optional<int64_t> duration;
1416                 for (auto i: reel->assets()) {
1417                         if (!duration) {
1418                                 duration = i->actual_duration();
1419                         } else if (*duration != i->actual_duration()) {
1420                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_ASSET_DURATION});
1421                                 break;
1422                         }
1423                 }
1424         }
1425
1426         if (reel->main_picture()) {
1427                 /* Check reel stuff */
1428                 auto const frame_rate = reel->main_picture()->frame_rate();
1429                 if (frame_rate.denominator != 1 ||
1430                     (frame_rate.numerator != 24 &&
1431                      frame_rate.numerator != 25 &&
1432                      frame_rate.numerator != 30 &&
1433                      frame_rate.numerator != 48 &&
1434                      frame_rate.numerator != 50 &&
1435                      frame_rate.numerator != 60 &&
1436                      frame_rate.numerator != 96)) {
1437                         notes.push_back({
1438                                 VerificationNote::Type::ERROR,
1439                                 VerificationNote::Code::INVALID_PICTURE_FRAME_RATE,
1440                                 String::compose("%1/%2", frame_rate.numerator, frame_rate.denominator)
1441                         });
1442                 }
1443                 /* Check asset */
1444                 if (reel->main_picture()->asset_ref().resolved()) {
1445                         verify_main_picture_asset(dcp, reel->main_picture(), stage, progress, options, notes);
1446                         auto const asset_size = reel->main_picture()->asset()->size();
1447                         if (main_picture_active_area) {
1448                                 if (main_picture_active_area->width > asset_size.width) {
1449                                         notes.push_back({
1450                                                         VerificationNote::Type::ERROR,
1451                                                         VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1452                                                         String::compose("width %1 is bigger than the asset width %2", main_picture_active_area->width, asset_size.width),
1453                                                         cpl->file().get()
1454                                                         });
1455                                 }
1456                                 if (main_picture_active_area->height > asset_size.height) {
1457                                         notes.push_back({
1458                                                         VerificationNote::Type::ERROR,
1459                                                         VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1460                                                         String::compose("height %1 is bigger than the asset height %2", main_picture_active_area->height, asset_size.height),
1461                                                         cpl->file().get()
1462                                                         });
1463                                 }
1464                         }
1465                 }
1466
1467         }
1468
1469         if (reel->main_sound() && reel->main_sound()->asset_ref().resolved()) {
1470                 verify_main_sound_asset(dcp, reel->main_sound(), stage, progress, options, notes, state);
1471         }
1472
1473         if (reel->main_subtitle()) {
1474                 verify_main_subtitle_reel(reel->main_subtitle(), notes);
1475                 if (reel->main_subtitle()->asset_ref().resolved()) {
1476                         verify_subtitle_asset(reel->main_subtitle()->asset(), reel->main_subtitle()->duration(), stage, xsd_dtd_directory, notes, state);
1477                 }
1478                 *have_main_subtitle = true;
1479         } else {
1480                 *have_no_main_subtitle = true;
1481         }
1482
1483         for (auto i: reel->closed_captions()) {
1484                 verify_closed_caption_reel(i, notes);
1485                 if (i->asset_ref().resolved()) {
1486                         verify_closed_caption_asset(i->asset(), i->duration(), stage, xsd_dtd_directory, notes);
1487                 }
1488         }
1489
1490         if (reel->main_markers()) {
1491                 for (auto const& i: reel->main_markers()->get()) {
1492                         markers_seen->insert(i);
1493                 }
1494                 if (reel->main_markers()->entry_point()) {
1495                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_ENTRY_POINT});
1496                 }
1497                 if (reel->main_markers()->duration()) {
1498                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::UNEXPECTED_DURATION});
1499                 }
1500         }
1501
1502         *fewest_closed_captions = std::min(*fewest_closed_captions, reel->closed_captions().size());
1503         *most_closed_captions = std::max(*most_closed_captions, reel->closed_captions().size());
1504
1505 }
1506
1507
1508 static
1509 void
1510 verify_cpl(
1511         shared_ptr<const DCP> dcp,
1512         shared_ptr<const CPL> cpl,
1513         function<void (string, optional<boost::filesystem::path>)> stage,
1514         boost::filesystem::path xsd_dtd_directory,
1515         function<void (float)> progress,
1516         VerificationOptions options,
1517         vector<VerificationNote>& notes,
1518         State& state
1519         )
1520 {
1521         stage("Checking CPL", cpl->file());
1522         validate_xml(cpl->file().get(), xsd_dtd_directory, notes);
1523
1524         if (cpl->any_encrypted() && !cpl->all_encrypted()) {
1525                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::PARTIALLY_ENCRYPTED});
1526         }
1527
1528         for (auto const& i: cpl->additional_subtitle_languages()) {
1529                 verify_language_tag(i, notes);
1530         }
1531
1532         if (!cpl->content_kind().scope() || *cpl->content_kind().scope() == "http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content") {
1533                 /* This is a content kind from http://www.smpte-ra.org/schemas/429-7/2006/CPL#standard-content; make sure it's one
1534                  * of the approved ones.
1535                  */
1536                 auto all = ContentKind::all();
1537                 auto name = cpl->content_kind().name();
1538                 transform(name.begin(), name.end(), name.begin(), ::tolower);
1539                 auto iter = std::find_if(all.begin(), all.end(), [name](ContentKind const& k) { return !k.scope() && k.name() == name; });
1540                 if (iter == all.end()) {
1541                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::INVALID_CONTENT_KIND, cpl->content_kind().name()});
1542                 }
1543         }
1544
1545         if (cpl->release_territory()) {
1546                 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") {
1547                         auto terr = cpl->release_territory().get();
1548                         /* Must be a valid region tag, or "001" */
1549                         try {
1550                                 LanguageTag::RegionSubtag test(terr);
1551                         } catch (...) {
1552                                 if (terr != "001") {
1553                                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_LANGUAGE, terr});
1554                                 }
1555                         }
1556                 }
1557         }
1558
1559         for (auto version: cpl->content_versions()) {
1560                 if (version.label_text.empty()) {
1561                         notes.push_back(
1562                                 dcp::VerificationNote(VerificationNote::Type::WARNING, VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get()).set_id(cpl->id())
1563                                 );
1564                         break;
1565                 }
1566         }
1567
1568         if (dcp->standard() == Standard::SMPTE) {
1569                 if (!cpl->annotation_text()) {
1570                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1571                 } else if (cpl->annotation_text().get() != cpl->content_title_text()) {
1572                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), cpl->file().get()});
1573                 }
1574         }
1575
1576         for (auto i: dcp->pkls()) {
1577                 /* Check that the CPL's hash corresponds to the PKL */
1578                 optional<string> h = i->hash(cpl->id());
1579                 if (h && make_digest(ArrayData(*cpl->file())) != *h) {
1580                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get()});
1581                 }
1582
1583                 /* Check that any PKL with a single CPL has its AnnotationText the same as the CPL's ContentTitleText */
1584                 optional<string> required_annotation_text;
1585                 for (auto j: i->assets()) {
1586                         /* See if this is a CPL */
1587                         for (auto k: dcp->cpls()) {
1588                                 if (j->id() == k->id()) {
1589                                         if (!required_annotation_text) {
1590                                                 /* First CPL we have found; this is the required AnnotationText unless we find another */
1591                                                 required_annotation_text = cpl->content_title_text();
1592                                         } else {
1593                                                 /* There's more than one CPL so we don't care what the PKL's AnnotationText is */
1594                                                 required_annotation_text = boost::none;
1595                                         }
1596                                 }
1597                         }
1598                 }
1599
1600                 if (required_annotation_text && i->annotation_text() != required_annotation_text) {
1601                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, i->id(), i->file().get()});
1602                 }
1603         }
1604
1605         /* set to true if any reel has a MainSubtitle */
1606         auto have_main_subtitle = false;
1607         /* set to true if any reel has no MainSubtitle */
1608         auto have_no_main_subtitle = false;
1609         /* fewest number of closed caption assets seen in a reel */
1610         size_t fewest_closed_captions = SIZE_MAX;
1611         /* most number of closed caption assets seen in a reel */
1612         size_t most_closed_captions = 0;
1613         map<Marker, Time> markers_seen;
1614
1615         auto const main_picture_active_area = cpl->main_picture_active_area();
1616         if (main_picture_active_area && (main_picture_active_area->width % 2)) {
1617                 notes.push_back({
1618                                 VerificationNote::Type::ERROR,
1619                                 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1620                                 String::compose("width %1 is not a multiple of 2", main_picture_active_area->width),
1621                                 cpl->file().get()
1622                         });
1623         }
1624         if (main_picture_active_area && (main_picture_active_area->height % 2)) {
1625                 notes.push_back({
1626                                 VerificationNote::Type::ERROR,
1627                                 VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA,
1628                                 String::compose("height %1 is not a multiple of 2", main_picture_active_area->height),
1629                                 cpl->file().get()
1630                         });
1631         }
1632
1633         for (auto reel: cpl->reels()) {
1634                 stage("Checking reel", optional<boost::filesystem::path>());
1635                 verify_reel(
1636                         dcp,
1637                         cpl,
1638                         reel,
1639                         main_picture_active_area,
1640                         stage,
1641                         xsd_dtd_directory,
1642                         progress,
1643                         options,
1644                         notes,
1645                         state,
1646                         &have_main_subtitle,
1647                         &have_no_main_subtitle,
1648                         &most_closed_captions,
1649                         &fewest_closed_captions,
1650                         &markers_seen
1651                         );
1652         }
1653
1654         verify_text_details(dcp->standard().get_value_or(dcp::Standard::SMPTE), cpl->reels(), notes);
1655
1656         if (dcp->standard() == Standard::SMPTE) {
1657                 if (auto msc = cpl->main_sound_configuration()) {
1658                         if (state.audio_channels && msc->channels() != *state.audio_channels) {
1659                                 notes.push_back({
1660                                                 VerificationNote::Type::ERROR,
1661                                                 VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION,
1662                                                 String::compose("MainSoundConfiguration has %1 channels but sound assets have %2", msc->channels(), *state.audio_channels),
1663                                                 cpl->file().get()
1664                                         });
1665                         }
1666                 }
1667
1668                 if (have_main_subtitle && have_no_main_subtitle) {
1669                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS});
1670                 }
1671
1672                 if (fewest_closed_captions != most_closed_captions) {
1673                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS});
1674                 }
1675
1676                 if (cpl->content_kind() == ContentKind::FEATURE) {
1677                         if (markers_seen.find(Marker::FFEC) == markers_seen.end()) {
1678                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFEC_IN_FEATURE});
1679                         }
1680                         if (markers_seen.find(Marker::FFMC) == markers_seen.end()) {
1681                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_FFMC_IN_FEATURE});
1682                         }
1683                 }
1684
1685                 auto ffoc = markers_seen.find(Marker::FFOC);
1686                 if (ffoc == markers_seen.end()) {
1687                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_FFOC});
1688                 } else if (ffoc->second.e != 1) {
1689                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_FFOC, raw_convert<string>(ffoc->second.e)});
1690                 }
1691
1692                 auto lfoc = markers_seen.find(Marker::LFOC);
1693                 if (lfoc == markers_seen.end()) {
1694                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::MISSING_LFOC});
1695                 } else {
1696                         auto lfoc_time = lfoc->second.as_editable_units_ceil(lfoc->second.tcr);
1697                         if (lfoc_time != (cpl->reels().back()->duration() - 1)) {
1698                                 notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INCORRECT_LFOC, raw_convert<string>(lfoc_time)});
1699                         }
1700                 }
1701
1702                 LinesCharactersResult result;
1703                 for (auto reel: cpl->reels()) {
1704                         if (reel->main_subtitle() && reel->main_subtitle()->asset()) {
1705                                 verify_text_lines_and_characters(reel->main_subtitle()->asset(), 52, 79, &result);
1706                         }
1707                 }
1708
1709                 if (result.line_count_exceeded) {
1710                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT});
1711                 }
1712                 if (result.error_length_exceeded) {
1713                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH});
1714                 } else if (result.warning_length_exceeded) {
1715                         notes.push_back({VerificationNote::Type::WARNING, VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH});
1716                 }
1717
1718                 result = LinesCharactersResult();
1719                 for (auto reel: cpl->reels()) {
1720                         for (auto i: reel->closed_captions()) {
1721                                 if (i->asset()) {
1722                                         verify_text_lines_and_characters(i->asset(), 32, 32, &result);
1723                                 }
1724                         }
1725                 }
1726
1727                 if (result.line_count_exceeded) {
1728                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT});
1729                 }
1730                 if (result.error_length_exceeded) {
1731                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH});
1732                 }
1733
1734                 if (!cpl->read_composition_metadata()) {
1735                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get()});
1736                 } else if (!cpl->version_number()) {
1737                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get()});
1738                 }
1739
1740                 verify_extension_metadata(cpl, notes);
1741
1742                 if (cpl->any_encrypted()) {
1743                         cxml::Document doc("CompositionPlaylist");
1744                         DCP_ASSERT(cpl->file());
1745                         doc.read_file(dcp::filesystem::fix_long_path(cpl->file().get()));
1746                         if (!doc.optional_node_child("Signature")) {
1747                                 notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, cpl->id(), cpl->file().get()});
1748                         }
1749                 }
1750         }
1751 }
1752
1753
1754 static
1755 void
1756 verify_pkl(
1757         shared_ptr<const DCP> dcp,
1758         shared_ptr<const PKL> pkl,
1759         boost::filesystem::path xsd_dtd_directory,
1760         vector<VerificationNote>& notes
1761         )
1762 {
1763         validate_xml(pkl->file().get(), xsd_dtd_directory, notes);
1764
1765         if (pkl_has_encrypted_assets(dcp, pkl)) {
1766                 cxml::Document doc("PackingList");
1767                 doc.read_file(dcp::filesystem::fix_long_path(pkl->file().get()));
1768                 if (!doc.optional_node_child("Signature")) {
1769                         notes.push_back({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, pkl->id(), pkl->file().get()});
1770                 }
1771         }
1772
1773         set<string> uuid_set;
1774         for (auto asset: pkl->assets()) {
1775                 if (!uuid_set.insert(asset->id()).second) {
1776                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl->id(), pkl->file().get()});
1777                         break;
1778                 }
1779         }
1780 }
1781
1782
1783
1784 static
1785 void
1786 verify_assetmap(
1787         shared_ptr<const DCP> dcp,
1788         boost::filesystem::path xsd_dtd_directory,
1789         vector<VerificationNote>& notes
1790         )
1791 {
1792         auto asset_map = dcp->asset_map();
1793         DCP_ASSERT(asset_map);
1794
1795         validate_xml(asset_map->file().get(), xsd_dtd_directory, notes);
1796
1797         set<string> uuid_set;
1798         for (auto const& asset: asset_map->assets()) {
1799                 if (!uuid_set.insert(asset.id()).second) {
1800                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map->id(), asset_map->file().get()});
1801                         break;
1802                 }
1803         }
1804 }
1805
1806
1807 vector<VerificationNote>
1808 dcp::verify (
1809         vector<boost::filesystem::path> directories,
1810         vector<dcp::DecryptedKDM> kdms,
1811         function<void (string, optional<boost::filesystem::path>)> stage,
1812         function<void (float)> progress,
1813         VerificationOptions options,
1814         optional<boost::filesystem::path> xsd_dtd_directory
1815         )
1816 {
1817         if (!xsd_dtd_directory) {
1818                 xsd_dtd_directory = resources_directory() / "xsd";
1819         }
1820         *xsd_dtd_directory = filesystem::canonical(*xsd_dtd_directory);
1821
1822         vector<VerificationNote> notes;
1823         State state{};
1824
1825         vector<shared_ptr<DCP>> dcps;
1826         for (auto i: directories) {
1827                 dcps.push_back (make_shared<DCP>(i));
1828         }
1829
1830         for (auto dcp: dcps) {
1831                 stage ("Checking DCP", dcp->directory());
1832                 bool carry_on = true;
1833                 try {
1834                         dcp->read (&notes, true);
1835                 } catch (MissingAssetmapError& e) {
1836                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1837                         carry_on = false;
1838                 } catch (ReadError& e) {
1839                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1840                 } catch (XMLError& e) {
1841                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1842                 } catch (MXFFileError& e) {
1843                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1844                 } catch (BadURNUUIDError& e) {
1845                         notes.push_back({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1846                 } catch (cxml::Error& e) {
1847                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::FAILED_READ, string(e.what())});
1848                 }
1849
1850                 if (!carry_on) {
1851                         continue;
1852                 }
1853
1854                 if (dcp->standard() != Standard::SMPTE) {
1855                         notes.push_back ({VerificationNote::Type::BV21_ERROR, VerificationNote::Code::INVALID_STANDARD});
1856                 }
1857
1858                 for (auto kdm: kdms) {
1859                         dcp->add(kdm);
1860                 }
1861
1862                 for (auto cpl: dcp->cpls()) {
1863                         verify_cpl(
1864                                 dcp,
1865                                 cpl,
1866                                 stage,
1867                                 *xsd_dtd_directory,
1868                                 progress,
1869                                 options,
1870                                 notes,
1871                                 state
1872                                 );
1873                 }
1874
1875                 for (auto pkl: dcp->pkls()) {
1876                         stage("Checking PKL", pkl->file());
1877                         verify_pkl(dcp, pkl, *xsd_dtd_directory, notes);
1878                 }
1879
1880                 if (dcp->asset_map_file()) {
1881                         stage("Checking ASSETMAP", dcp->asset_map_file().get());
1882                         verify_assetmap(dcp, *xsd_dtd_directory, notes);
1883                 } else {
1884                         notes.push_back ({VerificationNote::Type::ERROR, VerificationNote::Code::MISSING_ASSETMAP});
1885                 }
1886         }
1887
1888         return notes;
1889 }
1890
1891
1892 string
1893 dcp::note_to_string (VerificationNote note)
1894 {
1895         /** These strings should say what is wrong, incorporating any extra details (ID, filenames etc.).
1896          *
1897          *  e.g. "ClosedCaption asset has no <EntryPoint> tag.",
1898          *  not "ClosedCaption assets must have an <EntryPoint> tag."
1899          *
1900          *  It's OK to use XML tag names where they are clear.
1901          *  If both ID and filename are available, use only the ID.
1902          *  End messages with a full stop.
1903          *  Messages should not mention whether or not their errors are a part of Bv2.1.
1904          */
1905         switch (note.code()) {
1906         case VerificationNote::Code::FAILED_READ:
1907                 return *note.note();
1908         case VerificationNote::Code::MISMATCHED_CPL_HASHES:
1909                 return String::compose("The hash of the CPL %1 in the PKL does not agree with the CPL file.", note.note().get());
1910         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE:
1911                 return String::compose("The picture in a reel has an invalid frame rate %1.", note.note().get());
1912         case VerificationNote::Code::INCORRECT_PICTURE_HASH:
1913                 return String::compose("The hash of the picture asset %1 does not agree with the PKL file.", note.file()->filename());
1914         case VerificationNote::Code::MISMATCHED_PICTURE_HASHES:
1915                 return String::compose("The PKL and CPL hashes differ for the picture asset %1.", note.file()->filename());
1916         case VerificationNote::Code::INCORRECT_SOUND_HASH:
1917                 return String::compose("The hash of the sound asset %1 does not agree with the PKL file.", note.file()->filename());
1918         case VerificationNote::Code::MISMATCHED_SOUND_HASHES:
1919                 return String::compose("The PKL and CPL hashes differ for the sound asset %1.", note.file()->filename());
1920         case VerificationNote::Code::EMPTY_ASSET_PATH:
1921                 return "The asset map contains an empty asset path.";
1922         case VerificationNote::Code::MISSING_ASSET:
1923                 return String::compose("The file %1 for an asset in the asset map cannot be found.", note.file()->filename());
1924         case VerificationNote::Code::MISMATCHED_STANDARD:
1925                 return "The DCP contains both SMPTE and Interop parts.";
1926         case VerificationNote::Code::INVALID_XML:
1927                 return String::compose("An XML file is badly formed: %1 (%2:%3)", note.note().get(), note.file()->filename(), note.line().get());
1928         case VerificationNote::Code::MISSING_ASSETMAP:
1929                 return "No valid ASSETMAP or ASSETMAP.xml was found.";
1930         case VerificationNote::Code::INVALID_INTRINSIC_DURATION:
1931                 return String::compose("The intrinsic duration of the asset %1 is less than 1 second.", note.note().get());
1932         case VerificationNote::Code::INVALID_DURATION:
1933                 return String::compose("The duration of the asset %1 is less than 1 second.", note.note().get());
1934         case VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1935                 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());
1936         case VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES:
1937                 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());
1938         case VerificationNote::Code::EXTERNAL_ASSET:
1939                 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());
1940         case VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD:
1941                 return String::compose("The asset %1 is 3D but its MXF is marked as 2D.", note.file()->filename());
1942         case VerificationNote::Code::INVALID_STANDARD:
1943                 return "This DCP does not use the SMPTE standard.";
1944         case VerificationNote::Code::INVALID_LANGUAGE:
1945                 return String::compose("The DCP specifies a language '%1' which does not conform to the RFC 5646 standard.", note.note().get());
1946         case VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS:
1947                 return String::compose("The size %1 of picture asset %2 is not allowed.", note.note().get(), note.file()->filename());
1948         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K:
1949                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 2K DCPs.", note.note().get(), note.file()->filename());
1950         case VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K:
1951                 return String::compose("The frame rate %1 of picture asset %2 is not allowed for 4K DCPs.", note.note().get(), note.file()->filename());
1952         case VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D:
1953                 return "3D 4K DCPs are not allowed.";
1954         case VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES:
1955                 return String::compose("The size %1 of the closed caption asset %2 is larger than the 256KB maximum.", note.note().get(), note.file()->filename());
1956         case VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES:
1957                 return String::compose("The size %1 of the timed text asset %2 is larger than the 115MB maximum.", note.note().get(), note.file()->filename());
1958         case VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES:
1959                 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());
1960         case VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE:
1961                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <Language> tag.", note.file()->filename());
1962         case VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES:
1963                 return "Some subtitle assets have different <Language> tags than others";
1964         case VerificationNote::Code::MISSING_SUBTITLE_START_TIME:
1965                 return String::compose("The XML for the SMPTE subtitle asset %1 has no <StartTime> tag.", note.file()->filename());
1966         case VerificationNote::Code::INVALID_SUBTITLE_START_TIME:
1967                 return String::compose("The XML for a SMPTE subtitle asset %1 has a non-zero <StartTime> tag.", note.file()->filename());
1968         case VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME:
1969                 return "The first subtitle or closed caption is less than 4 seconds from the start of the DCP.";
1970         case VerificationNote::Code::INVALID_SUBTITLE_DURATION:
1971                 return "At least one subtitle lasts less than 15 frames.";
1972         case VerificationNote::Code::INVALID_SUBTITLE_SPACING:
1973                 return "At least one pair of subtitles is separated by less than 2 frames.";
1974         case VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY:
1975                 return "At least one subtitle extends outside of its reel.";
1976         case VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT:
1977                 return "There are more than 3 subtitle lines in at least one place in the DCP.";
1978         case VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH:
1979                 return "There are more than 52 characters in at least one subtitle line.";
1980         case VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH:
1981                 return "There are more than 79 characters in at least one subtitle line.";
1982         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT:
1983                 return "There are more than 3 closed caption lines in at least one place.";
1984         case VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH:
1985                 return "There are more than 32 characters in at least one closed caption line.";
1986         case VerificationNote::Code::INVALID_SOUND_FRAME_RATE:
1987                 return String::compose("The sound asset %1 has a sampling rate of %2", note.file()->filename(), note.note().get());
1988         case VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT:
1989                 return String::compose("The CPL %1 has no <AnnotationText> tag.", note.note().get());
1990         case VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT:
1991                 return String::compose("The CPL %1 has an <AnnotationText> which differs from its <ContentTitleText>.", note.note().get());
1992         case VerificationNote::Code::MISMATCHED_ASSET_DURATION:
1993                 return "All assets in a reel do not have the same duration.";
1994         case VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS:
1995                 return "At least one reel contains a subtitle asset, but some reel(s) do not.";
1996         case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS:
1997                 return "At least one reel has closed captions, but reels have different numbers of closed caption assets.";
1998         case VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT:
1999                 return String::compose("The subtitle asset %1 has no <EntryPoint> tag.", note.note().get());
2000         case VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT:
2001                 return String::compose("The subtitle asset %1 has an <EntryPoint> other than 0.", note.note().get());
2002         case VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT:
2003                 return String::compose("The closed caption asset %1 has no <EntryPoint> tag.", note.note().get());
2004         case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT:
2005                 return String::compose("The closed caption asset %1 has an <EntryPoint> other than 0.", note.note().get());
2006         case VerificationNote::Code::MISSING_HASH:
2007                 return String::compose("The asset %1 has no <Hash> tag in the CPL.", note.note().get());
2008         case VerificationNote::Code::MISSING_FFEC_IN_FEATURE:
2009                 return "The DCP is marked as a Feature but there is no FFEC (first frame of end credits) marker.";
2010         case VerificationNote::Code::MISSING_FFMC_IN_FEATURE:
2011                 return "The DCP is marked as a Feature but there is no FFMC (first frame of moving credits) marker.";
2012         case VerificationNote::Code::MISSING_FFOC:
2013                 return "There should be a FFOC (first frame of content) marker.";
2014         case VerificationNote::Code::MISSING_LFOC:
2015                 return "There should be a LFOC (last frame of content) marker.";
2016         case VerificationNote::Code::INCORRECT_FFOC:
2017                 return String::compose("The FFOC marker is %1 instead of 1", note.note().get());
2018         case VerificationNote::Code::INCORRECT_LFOC:
2019                 return String::compose("The LFOC marker is %1 instead of 1 less than the duration of the last reel.", note.note().get());
2020         case VerificationNote::Code::MISSING_CPL_METADATA:
2021                 return String::compose("The CPL %1 has no <CompositionMetadataAsset> tag.", note.note().get());
2022         case VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER:
2023                 return String::compose("The CPL %1 has no <VersionNumber> in its <CompositionMetadataAsset>.", note.note().get());
2024         case VerificationNote::Code::MISSING_EXTENSION_METADATA:
2025                 return String::compose("The CPL %1 has no <ExtensionMetadata> in its <CompositionMetadataAsset>.", note.note().get());
2026         case VerificationNote::Code::INVALID_EXTENSION_METADATA:
2027                 return String::compose("The CPL %1 has a malformed <ExtensionMetadata> (%2).", note.file()->filename(), note.note().get());
2028         case VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT:
2029                 return String::compose("The CPL %1, which has encrypted content, is not signed.", note.note().get());
2030         case VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT:
2031                 return String::compose("The PKL %1, which has encrypted content, is not signed.", note.note().get());
2032         case VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL:
2033                 return String::compose("The PKL %1 has only one CPL but its <AnnotationText> does not match the CPL's <ContentTitleText>.", note.note().get());
2034         case VerificationNote::Code::PARTIALLY_ENCRYPTED:
2035                 return "Some assets are encrypted but some are not.";
2036         case VerificationNote::Code::INVALID_JPEG2000_CODESTREAM:
2037                 return String::compose("The JPEG2000 codestream for at least one frame is invalid (%1).", note.note().get());
2038         case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K:
2039                 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 2K image instead of 1.", note.note().get());
2040         case VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_4K:
2041                 return String::compose("The JPEG2000 codestream uses %1 guard bits in a 4K image instead of 2.", note.note().get());
2042         case VerificationNote::Code::INVALID_JPEG2000_TILE_SIZE:
2043                 return "The JPEG2000 tile size is not the same as the image size.";
2044         case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_WIDTH:
2045                 return String::compose("The JPEG2000 codestream uses a code block width of %1 instead of 32.", note.note().get());
2046         case VerificationNote::Code::INVALID_JPEG2000_CODE_BLOCK_HEIGHT:
2047                 return String::compose("The JPEG2000 codestream uses a code block height of %1 instead of 32.", note.note().get());
2048         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_2K:
2049                 return String::compose("%1 POC markers found in 2K JPEG2000 codestream instead of 0.", note.note().get());
2050         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER_COUNT_FOR_4K:
2051                 return String::compose("%1 POC markers found in 4K JPEG2000 codestream instead of 1.", note.note().get());
2052         case VerificationNote::Code::INCORRECT_JPEG2000_POC_MARKER:
2053                 return String::compose("Incorrect POC marker content found (%1).", note.note().get());
2054         case VerificationNote::Code::INVALID_JPEG2000_POC_MARKER_LOCATION:
2055                 return "POC marker found outside main header.";
2056         case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_2K:
2057                 return String::compose("The JPEG2000 codestream has %1 tile parts in a 2K image instead of 3.", note.note().get());
2058         case VerificationNote::Code::INVALID_JPEG2000_TILE_PARTS_FOR_4K:
2059                 return String::compose("The JPEG2000 codestream has %1 tile parts in a 4K image instead of 6.", note.note().get());
2060         case VerificationNote::Code::MISSING_JPEG200_TLM_MARKER:
2061                 return "No TLM marker was found in a JPEG2000 codestream.";
2062         case VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID:
2063                 return "The Resource ID in a timed text MXF did not match the ID of the contained XML.";
2064         case VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID:
2065                 return "The Asset ID in a timed text MXF is the same as the Resource ID or that of the contained XML.";
2066         case VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION:
2067         {
2068                 vector<string> parts;
2069                 boost::split (parts, note.note().get(), boost::is_any_of(" "));
2070                 DCP_ASSERT (parts.size() == 2);
2071                 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]);
2072         }
2073         case VerificationNote::Code::MISSED_CHECK_OF_ENCRYPTED:
2074                 return "Some aspect of this DCP could not be checked because it is encrypted.";
2075         case VerificationNote::Code::EMPTY_TEXT:
2076                 return "There is an empty <Text> node in a subtitle or closed caption.";
2077         case VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN:
2078                 return "Some closed <Text> or <Image> nodes have different vertical alignments within a <Subtitle>.";
2079         case VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING:
2080                 return "Some closed captions are not listed in the order of their vertical position.";
2081         case VerificationNote::Code::UNEXPECTED_ENTRY_POINT:
2082                 return "There is an <EntryPoint> node inside a <MainMarkers>.";
2083         case VerificationNote::Code::UNEXPECTED_DURATION:
2084                 return "There is an <Duration> node inside a <MainMarkers>.";
2085         case VerificationNote::Code::INVALID_CONTENT_KIND:
2086                 return String::compose("<ContentKind> has an invalid value %1.", note.note().get());
2087         case VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA:
2088                 return String::compose("<MainPictureActiveaArea> has an invalid value: %1", note.note().get());
2089         case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL:
2090                 return String::compose("The PKL %1 has more than one asset with the same ID.", note.note().get());
2091         case VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP:
2092                 return String::compose("The ASSETMAP %1 has more than one asset with the same ID.", note.note().get());
2093         case VerificationNote::Code::MISSING_SUBTITLE:
2094                 return String::compose("The subtitle asset %1 has no subtitles.", note.note().get());
2095         case VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE:
2096                 return String::compose("<IssueDate> has an invalid value: %1", note.note().get());
2097         case VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS:
2098                 return String::compose("The sound assets do not all have the same channel count; the first to differ is %1", note.file()->filename());
2099         case VerificationNote::Code::INVALID_MAIN_SOUND_CONFIGURATION:
2100                 return String::compose("<MainSoundConfiguration> has an invalid value: %1", note.note().get());
2101         case VerificationNote::Code::MISSING_FONT:
2102                 return String::compose("The font file for font ID \"%1\" was not found, or was not referred to in the ASSETMAP.", note.note().get());
2103         case VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE:
2104                 return String::compose(
2105                         "Frame %1 has an image component that is too large (component %2 is %3 bytes in size).",
2106                         note.frame().get(), note.component().get(), note.size().get()
2107                         );
2108         case VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT:
2109                 return String::compose("The XML in the subtitle asset %1 has more than one namespace declaration.", note.note().get());
2110         case VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT:
2111                 return String::compose("A subtitle or closed caption refers to a font with ID %1 that does not have a corresponding <LoadFont> node", note.id().get());
2112         case VerificationNote::Code::MISSING_LOAD_FONT:
2113                 return String::compose("The SMPTE subtitle asset %1 has <Text> nodes but no <LoadFont> node", note.id().get());
2114         case VerificationNote::Code::MISMATCHED_ASSET_MAP_ID:
2115                 return String::compose("The asset with ID %1 in the asset map actually has an id of %2", note.id().get(), note.other_id().get());
2116         case VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT:
2117                 return String::compose("The <LabelText> in a <ContentVersion> in CPL %1 is empty", note.id().get());
2118         }
2119
2120         return "";
2121 }
2122
2123
2124 bool
2125 dcp::operator== (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2126 {
2127         return a.type() == b.type() && a.code() == b.code() && a.note() == b.note() && a.file() == b.file() && a.line() == b.line();
2128 }
2129
2130
2131 bool
2132 dcp::operator< (dcp::VerificationNote const& a, dcp::VerificationNote const& b)
2133 {
2134         if (a.type() != b.type()) {
2135                 return a.type() < b.type();
2136         }
2137
2138         if (a.code() != b.code()) {
2139                 return a.code() < b.code();
2140         }
2141
2142         if (a.note() != b.note()) {
2143                 return a.note().get_value_or("") < b.note().get_value_or("");
2144         }
2145
2146         if (a.file() != b.file()) {
2147                 return a.file().get_value_or("") < b.file().get_value_or("");
2148         }
2149
2150         return a.line().get_value_or(0) < b.line().get_value_or(0);
2151 }
2152
2153
2154 std::ostream&
2155 dcp::operator<< (std::ostream& s, dcp::VerificationNote const& note)
2156 {
2157         s << note_to_string (note);
2158         if (note.note()) {
2159                 s << " [" << note.note().get() << "]";
2160         }
2161         if (note.file()) {
2162                 s << " [" << note.file().get() << "]";
2163         }
2164         if (note.line()) {
2165                 s << " [" << note.line().get() << "]";
2166         }
2167         return s;
2168 }
2169