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