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