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