Check that Interop subtitle files have at least one subtitle.
[libdcp.git] / test / verify_test.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 #include "compose.hpp"
36 #include "cpl.h"
37 #include "dcp.h"
38 #include "file.h"
39 #include "interop_subtitle_asset.h"
40 #include "j2k_transcode.h"
41 #include "mono_picture_asset.h"
42 #include "mono_picture_asset_writer.h"
43 #include "openjpeg_image.h"
44 #include "raw_convert.h"
45 #include "reel.h"
46 #include "reel_interop_closed_caption_asset.h"
47 #include "reel_interop_subtitle_asset.h"
48 #include "reel_markers_asset.h"
49 #include "reel_mono_picture_asset.h"
50 #include "reel_sound_asset.h"
51 #include "reel_stereo_picture_asset.h"
52 #include "reel_smpte_closed_caption_asset.h"
53 #include "reel_smpte_subtitle_asset.h"
54 #include "smpte_subtitle_asset.h"
55 #include "stereo_picture_asset.h"
56 #include "stream_operators.h"
57 #include "test.h"
58 #include "util.h"
59 #include "verify.h"
60 #include "verify_j2k.h"
61 #include <boost/test/unit_test.hpp>
62 #include <boost/algorithm/string.hpp>
63 #include <cstdio>
64 #include <iostream>
65
66
67 using std::list;
68 using std::make_pair;
69 using std::make_shared;
70 using std::pair;
71 using std::shared_ptr;
72 using std::string;
73 using std::vector;
74 using boost::optional;
75 using namespace boost::filesystem;
76
77
78 static list<pair<string, optional<path>>> stages;
79
80 static string filename_to_id(boost::filesystem::path path)
81 {
82         return path.string().substr(4, path.string().length() - 8);
83 }
84
85 static boost::filesystem::path const dcp_test1_pkl = find_file("test/ref/DCP/dcp_test1", "pkl_").filename();
86 static string const dcp_test1_pkl_id = filename_to_id(dcp_test1_pkl);
87
88 static boost::filesystem::path const dcp_test1_cpl = find_file("test/ref/DCP/dcp_test1", "cpl_").filename();
89 static string const dcp_test1_cpl_id = filename_to_id(dcp_test1_cpl);
90
91 static string const dcp_test1_asset_map_id = "017b3de4-6dda-408d-b19b-6711354b0bc3";
92
93 static boost::filesystem::path const encryption_test_cpl = find_file("test/ref/DCP/encryption_test", "cpl_").filename();
94 static string const encryption_test_cpl_id = filename_to_id(encryption_test_cpl);
95
96 static boost::filesystem::path const encryption_test_pkl = find_file("test/ref/DCP/encryption_test", "pkl_").filename();
97 static string const encryption_test_pkl_id = filename_to_id(encryption_test_pkl);
98
99 static void
100 stage (string s, optional<path> p)
101 {
102         stages.push_back (make_pair (s, p));
103 }
104
105 static void
106 progress (float)
107 {
108
109 }
110
111 static void
112 prepare_directory (path path)
113 {
114         using namespace boost::filesystem;
115         remove_all (path);
116         create_directories (path);
117 }
118
119
120 /** Copy dcp_test{reference_number} to build/test/verify_test{verify_test_suffix}
121  *  to make a new sacrifical test DCP.
122  */
123 static path
124 setup (int reference_number, string verify_test_suffix)
125 {
126         auto const dir = dcp::String::compose("build/test/verify_test%1", verify_test_suffix);
127         prepare_directory (dir);
128         for (auto i: directory_iterator(dcp::String::compose("test/ref/DCP/dcp_test%1", reference_number))) {
129                 copy_file (i.path(), dir / i.path().filename());
130         }
131
132         return dir;
133 }
134
135
136 static
137 shared_ptr<dcp::CPL>
138 write_dcp_with_single_asset (path dir, shared_ptr<dcp::ReelAsset> reel_asset, dcp::Standard standard = dcp::Standard::SMPTE)
139 {
140         auto reel = make_shared<dcp::Reel>();
141         reel->add (reel_asset);
142         reel->add (simple_markers());
143
144         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, standard);
145         cpl->add (reel);
146         auto dcp = make_shared<dcp::DCP>(dir);
147         dcp->add (cpl);
148         dcp->set_annotation_text("hello");
149         dcp->write_xml ();
150
151         return cpl;
152 }
153
154
155 /** Class that can alter a file by searching and replacing strings within it.
156  *  On destruction modifies the file whose name was given to the constructor.
157  */
158 class Editor
159 {
160 public:
161         Editor (path path)
162                 : _path(path)
163         {
164                 _content = dcp::file_to_string (_path);
165         }
166
167         ~Editor ()
168         {
169                 auto f = fopen(_path.string().c_str(), "w");
170                 BOOST_REQUIRE (f);
171                 fwrite (_content.c_str(), _content.length(), 1, f);
172                 fclose (f);
173         }
174
175         class ChangeChecker
176         {
177         public:
178                 ChangeChecker(Editor* editor)
179                         : _editor(editor)
180                 {
181                         _old_content = _editor->_content;
182                 }
183
184                 ~ChangeChecker()
185                 {
186                         BOOST_REQUIRE(_old_content != _editor->_content);
187                 }
188         private:
189                 Editor* _editor;
190                 std::string _old_content;
191         };
192
193         void replace (string a, string b)
194         {
195                 ChangeChecker cc(this);
196                 boost::algorithm::replace_all (_content, a, b);
197         }
198
199         void delete_first_line_containing (string s)
200         {
201                 ChangeChecker cc(this);
202                 auto lines = as_lines();
203                 _content = "";
204                 bool done = false;
205                 for (auto i: lines) {
206                         if (i.find(s) == string::npos || done) {
207                                 _content += i + "\n";
208                         } else {
209                                 done = true;
210                         }
211                 }
212         }
213
214         void delete_lines (string from, string to)
215         {
216                 ChangeChecker cc(this);
217                 auto lines = as_lines();
218                 bool deleting = false;
219                 _content = "";
220                 for (auto i: lines) {
221                         if (i.find(from) != string::npos) {
222                                 deleting = true;
223                         }
224                         if (!deleting) {
225                                 _content += i + "\n";
226                         }
227                         if (deleting && i.find(to) != string::npos) {
228                                 deleting = false;
229                         }
230                 }
231         }
232
233         void insert (string after, string line)
234         {
235                 ChangeChecker cc(this);
236                 auto lines = as_lines();
237                 _content = "";
238                 bool replaced = false;
239                 for (auto i: lines) {
240                         _content += i + "\n";
241                         if (!replaced && i.find(after) != string::npos) {
242                                 _content += line + "\n";
243                                 replaced = true;
244                         }
245                 }
246         }
247
248         void delete_lines_after(string after, int lines_to_delete)
249         {
250                 ChangeChecker cc(this);
251                 auto lines = as_lines();
252                 _content = "";
253                 auto iter = std::find_if(lines.begin(), lines.end(), [after](string const& line) {
254                         return line.find(after) != string::npos;
255                 });
256                 int to_delete = 0;
257                 for (auto i = lines.begin(); i != lines.end(); ++i) {
258                         if (i == iter) {
259                                 to_delete = lines_to_delete;
260                                 _content += *i + "\n";
261                         } else if (to_delete == 0) {
262                                 _content += *i + "\n";
263                         } else {
264                                 --to_delete;
265                         }
266                 }
267         }
268
269 private:
270         friend class ChangeChecker;
271
272         vector<string> as_lines() const
273         {
274                 vector<string> lines;
275                 boost::algorithm::split(lines, _content, boost::is_any_of("\r\n"), boost::token_compress_on);
276                 return lines;
277         }
278
279         path _path;
280         std::string _content;
281 };
282
283
284 LIBDCP_DISABLE_WARNINGS
285 static
286 void
287 dump_notes (vector<dcp::VerificationNote> const & notes)
288 {
289         for (auto i: notes) {
290                 std::cout << dcp::note_to_string(i) << "\n";
291         }
292 }
293 LIBDCP_ENABLE_WARNINGS
294
295
296 static
297 void
298 check_verify_result (vector<path> dir, vector<dcp::VerificationNote> test_notes)
299 {
300         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
301         std::sort (notes.begin(), notes.end());
302         std::sort (test_notes.begin(), test_notes.end());
303
304         string message = "\nVerification notes from test:\n";
305         for (auto i: notes) {
306                 message += "  " + note_to_string(i) + "\n";
307                 message += dcp::String::compose(
308                         "  [%1 %2 %3 %4 %5]\n",
309                         static_cast<int>(i.type()),
310                         static_cast<int>(i.code()),
311                         i.note().get_value_or("<none>"),
312                         i.file().get_value_or("<none>"),
313                         i.line().get_value_or(0)
314                         );
315         }
316         message += "Expected:\n";
317         for (auto i: test_notes) {
318                 message += "  " + note_to_string(i) + "\n";
319                 message += dcp::String::compose(
320                         "  [%1 %2 %3 %4 %5]\n",
321                         static_cast<int>(i.type()),
322                         static_cast<int>(i.code()),
323                         i.note().get_value_or("<none>"),
324                         i.file().get_value_or("<none>"),
325                         i.line().get_value_or(0)
326                         );
327         }
328
329         BOOST_REQUIRE_MESSAGE (notes == test_notes, message);
330 }
331
332
333 /* Copy dcp_test1 to build/test/verify_test{suffix} then edit a file found by the functor 'file',
334  * replacing from with to.  Verify the resulting DCP and check that the results match the given
335  * list of codes.
336  */
337 static
338 void
339 check_verify_result_after_replace (string suffix, boost::function<path (string)> file, string from, string to, vector<dcp::VerificationNote::Code> codes)
340 {
341         auto dir = setup (1, suffix);
342
343         {
344                 Editor e (file(suffix));
345                 e.replace (from, to);
346         }
347
348         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
349
350         BOOST_REQUIRE_EQUAL (notes.size(), codes.size());
351         auto i = notes.begin();
352         auto j = codes.begin();
353         while (i != notes.end()) {
354                 BOOST_CHECK_EQUAL (i->code(), *j);
355                 ++i;
356                 ++j;
357         }
358 }
359
360
361 BOOST_AUTO_TEST_CASE (verify_no_error)
362 {
363         stages.clear ();
364         auto dir = setup (1, "no_error");
365         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
366
367         path const cpl_file = dir / dcp_test1_cpl;
368         path const pkl_file = dir / dcp_test1_pkl;
369         path const assetmap_file = dir / "ASSETMAP.xml";
370
371         auto st = stages.begin();
372         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
373         BOOST_REQUIRE (st->second);
374         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir));
375         ++st;
376         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
377         BOOST_REQUIRE (st->second);
378         BOOST_CHECK_EQUAL (st->second.get(), canonical(cpl_file));
379         ++st;
380         BOOST_CHECK_EQUAL (st->first, "Checking reel");
381         BOOST_REQUIRE (!st->second);
382         ++st;
383         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
384         BOOST_REQUIRE (st->second);
385         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "video.mxf"));
386         ++st;
387         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
388         BOOST_REQUIRE (st->second);
389         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "video.mxf"));
390         ++st;
391         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
392         BOOST_REQUIRE (st->second);
393         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "audio.mxf"));
394         ++st;
395         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
396         BOOST_REQUIRE (st->second);
397         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "audio.mxf"));
398         ++st;
399         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
400         BOOST_REQUIRE (st->second);
401         BOOST_CHECK_EQUAL (st->second.get(), canonical(pkl_file));
402         ++st;
403         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
404         BOOST_REQUIRE (st->second);
405         BOOST_CHECK_EQUAL (st->second.get(), canonical(assetmap_file));
406         ++st;
407         BOOST_REQUIRE (st == stages.end());
408
409         BOOST_CHECK_EQUAL (notes.size(), 0U);
410 }
411
412
413 BOOST_AUTO_TEST_CASE (verify_incorrect_picture_sound_hash)
414 {
415         using namespace boost::filesystem;
416
417         auto dir = setup (1, "incorrect_picture_sound_hash");
418
419         auto video_path = path(dir / "video.mxf");
420         auto mod = fopen(video_path.string().c_str(), "r+b");
421         BOOST_REQUIRE (mod);
422         fseek (mod, 4096, SEEK_SET);
423         int x = 42;
424         fwrite (&x, sizeof(x), 1, mod);
425         fclose (mod);
426
427         auto audio_path = path(dir / "audio.mxf");
428         mod = fopen(audio_path.string().c_str(), "r+b");
429         BOOST_REQUIRE (mod);
430         BOOST_REQUIRE_EQUAL (fseek(mod, -64, SEEK_END), 0);
431         BOOST_REQUIRE (fwrite (&x, sizeof(x), 1, mod) == 1);
432         fclose (mod);
433
434         dcp::ASDCPErrorSuspender sus;
435         check_verify_result (
436                 { dir },
437                 {
438                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_PICTURE_HASH, canonical(video_path) },
439                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_SOUND_HASH, canonical(audio_path) },
440                 });
441 }
442
443
444 BOOST_AUTO_TEST_CASE (verify_mismatched_picture_sound_hashes)
445 {
446         using namespace boost::filesystem;
447
448         auto dir = setup (1, "mismatched_picture_sound_hashes");
449
450         {
451                 Editor e (dir / dcp_test1_pkl);
452                 e.replace ("<Hash>", "<Hash>x");
453         }
454
455         check_verify_result (
456                 { dir },
457                 {
458                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id, canonical(dir / dcp_test1_cpl) },
459                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_PICTURE_HASHES, canonical(dir / "video.mxf") },
460                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_SOUND_HASHES, canonical(dir / "audio.mxf") },
461                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xKcJb7S2K5cNm8RG4kfQD5FTeS0A=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 28 },
462                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xtfX1mVIKJCVr1m7Y32Nzxf0+Rpw=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 12 },
463                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, "value 'xwUmt8G+cFFKMGt0ueS9+F1S4uhc=' is invalid Base64-encoded binary", canonical(dir / dcp_test1_pkl), 20 },
464                 });
465 }
466
467
468 BOOST_AUTO_TEST_CASE (verify_failed_read_content_kind)
469 {
470         auto dir = setup (1, "failed_read_content_kind");
471
472         {
473                 Editor e (dir / dcp_test1_cpl);
474                 e.replace ("<ContentKind>", "<ContentKind>x");
475         }
476
477         check_verify_result (
478                 { dir },
479                 {
480                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, dcp_test1_cpl_id, canonical(dir / dcp_test1_cpl) },
481                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_CONTENT_KIND, string("xtrailer") }
482                 });
483 }
484
485
486 static
487 path
488 cpl (string suffix)
489 {
490         return dcp::String::compose("build/test/verify_test%1/%2", suffix, dcp_test1_cpl);
491 }
492
493
494 static
495 path
496 pkl (string suffix)
497 {
498         return dcp::String::compose("build/test/verify_test%1/%2", suffix, dcp_test1_pkl);
499 }
500
501
502 static
503 path
504 asset_map (string suffix)
505 {
506         return dcp::String::compose("build/test/verify_test%1/ASSETMAP.xml", suffix);
507 }
508
509
510 BOOST_AUTO_TEST_CASE (verify_invalid_picture_frame_rate)
511 {
512         check_verify_result_after_replace (
513                         "invalid_picture_frame_rate", &cpl,
514                         "<FrameRate>24 1", "<FrameRate>99 1",
515                         { dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES,
516                           dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE }
517                         );
518 }
519
520 BOOST_AUTO_TEST_CASE (verify_missing_asset)
521 {
522         auto dir = setup (1, "missing_asset");
523         remove (dir / "video.mxf");
524         check_verify_result (
525                 { dir },
526                 {
527                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_ASSET, canonical(dir) / "video.mxf" }
528                 });
529 }
530
531
532 BOOST_AUTO_TEST_CASE (verify_empty_asset_path)
533 {
534         check_verify_result_after_replace (
535                         "empty_asset_path", &asset_map,
536                         "<Path>video.mxf</Path>", "<Path></Path>",
537                         { dcp::VerificationNote::Code::EMPTY_ASSET_PATH }
538                         );
539 }
540
541
542 BOOST_AUTO_TEST_CASE (verify_mismatched_standard)
543 {
544         check_verify_result_after_replace (
545                         "mismatched_standard", &cpl,
546                         "http://www.smpte-ra.org/schemas/429-7/2006/CPL", "http://www.digicine.com/PROTO-ASDCP-CPL-20040511#",
547                         { dcp::VerificationNote::Code::MISMATCHED_STANDARD,
548                           dcp::VerificationNote::Code::INVALID_XML,
549                           dcp::VerificationNote::Code::INVALID_XML,
550                           dcp::VerificationNote::Code::INVALID_XML,
551                           dcp::VerificationNote::Code::INVALID_XML,
552                           dcp::VerificationNote::Code::INVALID_XML,
553                           dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES }
554                         );
555 }
556
557
558 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_id)
559 {
560         /* There's no MISMATCHED_CPL_HASHES error here because it can't find the correct hash by ID (since the ID is wrong) */
561         check_verify_result_after_replace (
562                         "invalid_xml_cpl_id", &cpl,
563                         "<Id>urn:uuid:6affb8ee-0020-4dff-a53c-17652f6358ab", "<Id>urn:uuid:6affb8ee-0020-4dff-a53c-17652f6358a",
564                         { dcp::VerificationNote::Code::INVALID_XML }
565                         );
566 }
567
568
569 BOOST_AUTO_TEST_CASE (verify_invalid_xml_issue_date)
570 {
571         check_verify_result_after_replace (
572                         "invalid_xml_issue_date", &cpl,
573                         "<IssueDate>", "<IssueDate>x",
574                         { dcp::VerificationNote::Code::INVALID_XML,
575                           dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES }
576                         );
577 }
578
579
580 BOOST_AUTO_TEST_CASE (verify_invalid_xml_pkl_id)
581 {
582         check_verify_result_after_replace (
583                 "invalid_xml_pkl_id", &pkl,
584                 "<Id>urn:uuid:" + dcp_test1_pkl_id.substr(0, 3),
585                 "<Id>urn:uuid:x" + dcp_test1_pkl_id.substr(1, 2),
586                 { dcp::VerificationNote::Code::INVALID_XML }
587                 );
588 }
589
590
591 BOOST_AUTO_TEST_CASE (verify_invalid_xml_asset_map_id)
592 {
593         check_verify_result_after_replace (
594                 "invalid_xml_asset_map_id", &asset_map,
595                 "<Id>urn:uuid:" + dcp_test1_asset_map_id.substr(0, 3),
596                 "<Id>urn:uuid:x" + dcp_test1_asset_map_id.substr(1, 2),
597                 { dcp::VerificationNote::Code::INVALID_XML }
598                 );
599 }
600
601
602 BOOST_AUTO_TEST_CASE (verify_invalid_standard)
603 {
604         stages.clear ();
605         auto dir = setup (3, "verify_invalid_standard");
606         auto notes = dcp::verify ({dir}, &stage, &progress, xsd_test);
607
608         path const cpl_file = dir / "cpl_cbfd2bc0-21cf-4a8f-95d8-9cddcbe51296.xml";
609         path const pkl_file = dir / "pkl_d87a950c-bd6f-41f6-90cc-56ccd673e131.xml";
610         path const assetmap_file = dir / "ASSETMAP";
611
612         auto st = stages.begin();
613         BOOST_CHECK_EQUAL (st->first, "Checking DCP");
614         BOOST_REQUIRE (st->second);
615         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir));
616         ++st;
617         BOOST_CHECK_EQUAL (st->first, "Checking CPL");
618         BOOST_REQUIRE (st->second);
619         BOOST_CHECK_EQUAL (st->second.get(), canonical(cpl_file));
620         ++st;
621         BOOST_CHECK_EQUAL (st->first, "Checking reel");
622         BOOST_REQUIRE (!st->second);
623         ++st;
624         BOOST_CHECK_EQUAL (st->first, "Checking picture asset hash");
625         BOOST_REQUIRE (st->second);
626         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
627         ++st;
628         BOOST_CHECK_EQUAL (st->first, "Checking picture frame sizes");
629         BOOST_REQUIRE (st->second);
630         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "j2c_c6035f97-b07d-4e1c-944d-603fc2ddc242.mxf"));
631         ++st;
632         BOOST_CHECK_EQUAL (st->first, "Checking sound asset hash");
633         BOOST_REQUIRE (st->second);
634         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
635         ++st;
636         BOOST_CHECK_EQUAL (st->first, "Checking sound asset metadata");
637         BOOST_REQUIRE (st->second);
638         BOOST_CHECK_EQUAL (st->second.get(), canonical(dir / "pcm_69cf9eaf-9a99-4776-b022-6902208626c3.mxf"));
639         ++st;
640         BOOST_CHECK_EQUAL (st->first, "Checking PKL");
641         BOOST_REQUIRE (st->second);
642         BOOST_CHECK_EQUAL (st->second.get(), canonical(pkl_file));
643         ++st;
644         BOOST_CHECK_EQUAL (st->first, "Checking ASSETMAP");
645         BOOST_REQUIRE (st->second);
646         BOOST_CHECK_EQUAL (st->second.get(), canonical(assetmap_file));
647         ++st;
648         BOOST_REQUIRE (st == stages.end());
649
650         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
651         auto i = notes.begin ();
652         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::Type::BV21_ERROR);
653         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::Code::INVALID_STANDARD);
654         ++i;
655         BOOST_CHECK_EQUAL (i->type(), dcp::VerificationNote::Type::BV21_ERROR);
656         BOOST_CHECK_EQUAL (i->code(), dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K);
657 }
658
659 /* DCP with a short asset */
660 BOOST_AUTO_TEST_CASE (verify_invalid_duration)
661 {
662         auto dir = setup (8, "invalid_duration");
663         check_verify_result (
664                 { dir },
665                 {
666                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
667                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_DURATION, string("d7576dcb-a361-4139-96b8-267f5f8d7f91") },
668                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION, string("d7576dcb-a361-4139-96b8-267f5f8d7f91") },
669                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_DURATION, string("a2a87f5d-b749-4a7e-8d0c-9d48a4abf626") },
670                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_INTRINSIC_DURATION, string("a2a87f5d-b749-4a7e-8d0c-9d48a4abf626") },
671                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_GUARD_BITS_FOR_2K, string("2") }
672                 });
673 }
674
675
676 static
677 shared_ptr<dcp::CPL>
678 dcp_from_frame (dcp::ArrayData const& frame, path dir)
679 {
680         auto asset = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
681         create_directories (dir);
682         auto writer = asset->start_write (dir / "pic.mxf", true);
683         for (int i = 0; i < 24; ++i) {
684                 writer->write (frame.data(), frame.size());
685         }
686         writer->finalize ();
687
688         auto reel_asset = make_shared<dcp::ReelMonoPictureAsset>(asset, 0);
689         return write_dcp_with_single_asset (dir, reel_asset);
690 }
691
692
693 BOOST_AUTO_TEST_CASE (verify_invalid_picture_frame_size_in_bytes)
694 {
695         int const too_big = 1302083 * 2;
696
697         /* Compress a black image */
698         auto image = black_image ();
699         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
700         BOOST_REQUIRE (frame.size() < too_big);
701
702         /* Place it in a bigger block with some zero padding at the end */
703         dcp::ArrayData oversized_frame(too_big);
704         memcpy (oversized_frame.data(), frame.data(), frame.size());
705         memset (oversized_frame.data() + frame.size(), 0, too_big - frame.size());
706
707         path const dir("build/test/verify_invalid_picture_frame_size_in_bytes");
708         prepare_directory (dir);
709         auto cpl = dcp_from_frame (oversized_frame, dir);
710
711         check_verify_result (
712                 { dir },
713                 {
714                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM, string("missing marker start byte") },
715                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(dir / "pic.mxf") },
716                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
717                 });
718 }
719
720
721 BOOST_AUTO_TEST_CASE (verify_nearly_invalid_picture_frame_size_in_bytes)
722 {
723         int const nearly_too_big = 1302083 * 0.98;
724
725         /* Compress a black image */
726         auto image = black_image ();
727         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
728         BOOST_REQUIRE (frame.size() < nearly_too_big);
729
730         /* Place it in a bigger block with some zero padding at the end */
731         dcp::ArrayData oversized_frame(nearly_too_big);
732         memcpy (oversized_frame.data(), frame.data(), frame.size());
733         memset (oversized_frame.data() + frame.size(), 0, nearly_too_big - frame.size());
734
735         path const dir("build/test/verify_nearly_invalid_picture_frame_size_in_bytes");
736         prepare_directory (dir);
737         auto cpl = dcp_from_frame (oversized_frame, dir);
738
739         check_verify_result (
740                 { dir },
741                 {
742                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_CODESTREAM, string("missing marker start byte") },
743                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(dir / "pic.mxf") },
744                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
745                 });
746 }
747
748
749 BOOST_AUTO_TEST_CASE (verify_valid_picture_frame_size_in_bytes)
750 {
751         /* Compress a black image */
752         auto image = black_image ();
753         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
754         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
755
756         path const dir("build/test/verify_valid_picture_frame_size_in_bytes");
757         prepare_directory (dir);
758         auto cpl = dcp_from_frame (frame, dir);
759
760         check_verify_result ({ dir }, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
761 }
762
763
764 BOOST_AUTO_TEST_CASE (verify_valid_interop_subtitles)
765 {
766         path const dir("build/test/verify_valid_interop_subtitles");
767         prepare_directory (dir);
768         copy_file ("test/data/subs1.xml", dir / "subs.xml");
769         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
770         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
771         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
772
773         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD }});
774 }
775
776
777 BOOST_AUTO_TEST_CASE (verify_invalid_interop_subtitles)
778 {
779         using namespace boost::filesystem;
780
781         path const dir("build/test/verify_invalid_interop_subtitles");
782         prepare_directory (dir);
783         copy_file ("test/data/subs1.xml", dir / "subs.xml");
784         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
785         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
786         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
787
788         {
789                 Editor e (dir / "subs.xml");
790                 e.replace ("</ReelNumber>", "</ReelNumber><Foo></Foo>");
791         }
792
793         check_verify_result (
794                 { dir },
795                 {
796                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
797                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'Foo'"), path(), 5 },
798                         {
799                                 dcp::VerificationNote::Type::ERROR,
800                                 dcp::VerificationNote::Code::INVALID_XML,
801                                 string("element 'Foo' is not allowed for content model '(SubtitleID,MovieTitle,ReelNumber,Language,LoadFont*,Font*,Subtitle*)'"),
802                                 path(),
803                                 29
804                         }
805                 });
806 }
807
808
809 BOOST_AUTO_TEST_CASE(verify_interop_subtitle_asset_with_no_subtitles)
810 {
811         path const dir("build/test/verify_interop_subtitle_asset_with_no_subtitles");
812         prepare_directory(dir);
813         copy_file("test/data/subs4.xml", dir / "subs.xml");
814         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
815         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
816         write_dcp_with_single_asset(dir, reel_asset, dcp::Standard::INTEROP);
817
818         check_verify_result (
819                 { dir },
820                 {
821                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
822                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE, asset->id(), boost::filesystem::canonical(asset->file().get()) },
823                 });
824
825 }
826
827
828 BOOST_AUTO_TEST_CASE (verify_valid_smpte_subtitles)
829 {
830         path const dir("build/test/verify_valid_smpte_subtitles");
831         prepare_directory (dir);
832         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
833         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
834         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
835         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
836
837         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
838 }
839
840
841 BOOST_AUTO_TEST_CASE (verify_invalid_smpte_subtitles)
842 {
843         using namespace boost::filesystem;
844
845         path const dir("build/test/verify_invalid_smpte_subtitles");
846         prepare_directory (dir);
847         /* This broken_smpte.mxf does not use urn:uuid: for its subtitle ID, which we tolerate (rightly or wrongly) */
848         copy_file ("test/data/broken_smpte.mxf", dir / "subs.mxf");
849         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
850         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
851         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
852
853         check_verify_result (
854                 { dir },
855                 {
856                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'Foo'"), path(), 2 },
857                         {
858                                 dcp::VerificationNote::Type::ERROR,
859                                 dcp::VerificationNote::Code::INVALID_XML,
860                                 string("element 'Foo' is not allowed for content model '(Id,ContentTitleText,AnnotationText?,IssueDate,ReelNumber?,Language?,EditRate,TimeCodeRate,StartTime?,DisplayType?,LoadFont*,SubtitleList)'"),
861                                 path(),
862                                 2
863                         },
864                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
865                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
866                 });
867 }
868
869
870 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles)
871 {
872         path const dir("build/test/verify_empty_text_node_in_subtitles");
873         prepare_directory (dir);
874         copy_file ("test/data/empty_text.mxf", dir / "subs.mxf");
875         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
876         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
877         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
878
879         check_verify_result (
880                 { dir },
881                 {
882                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_TEXT },
883                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
884                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
885                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
886                 });
887 }
888
889
890 /** A <Text> node with no content except some <Font> nodes, which themselves do have content */
891 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles_with_child_nodes)
892 {
893         path const dir("build/test/verify_empty_text_node_in_subtitles_with_child_nodes");
894         prepare_directory (dir);
895         copy_file ("test/data/empty_but_with_children.xml", dir / "subs.xml");
896         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
897         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
898         auto cpl = write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
899
900         check_verify_result (
901                 { dir },
902                 {
903                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
904                 });
905 }
906
907
908 /** A <Text> node with no content except some <Font> nodes, which themselves also have no content */
909 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles_with_empty_child_nodes)
910 {
911         path const dir("build/test/verify_empty_text_node_in_subtitles_with_empty_child_nodes");
912         prepare_directory (dir);
913         copy_file ("test/data/empty_with_empty_children.xml", dir / "subs.xml");
914         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
915         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
916         auto cpl = write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
917
918         check_verify_result (
919                 { dir },
920                 {
921                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
922                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_TEXT },
923                 });
924 }
925
926
927 BOOST_AUTO_TEST_CASE (verify_external_asset)
928 {
929         path const ov_dir("build/test/verify_external_asset");
930         prepare_directory (ov_dir);
931
932         auto image = black_image ();
933         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
934         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
935         dcp_from_frame (frame, ov_dir);
936
937         dcp::DCP ov (ov_dir);
938         ov.read ();
939
940         path const vf_dir("build/test/verify_external_asset_vf");
941         prepare_directory (vf_dir);
942
943         auto picture = ov.cpls()[0]->reels()[0]->main_picture();
944         auto cpl = write_dcp_with_single_asset (vf_dir, picture);
945
946         check_verify_result (
947                 { vf_dir },
948                 {
949                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, picture->asset()->id() },
950                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
951                 });
952 }
953
954
955 BOOST_AUTO_TEST_CASE (verify_valid_cpl_metadata)
956 {
957         path const dir("build/test/verify_valid_cpl_metadata");
958         prepare_directory (dir);
959
960         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
961         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
962         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
963
964         auto reel = make_shared<dcp::Reel>();
965         reel->add (reel_asset);
966
967         reel->add (make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", 16 * 24), 0));
968         reel->add (simple_markers(16 * 24));
969
970         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
971         cpl->add (reel);
972         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
973         cpl->set_main_sound_sample_rate (48000);
974         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
975         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
976         cpl->set_version_number (1);
977
978         dcp::DCP dcp (dir);
979         dcp.add (cpl);
980         dcp.set_annotation_text("hello");
981         dcp.write_xml ();
982 }
983
984
985 path
986 find_prefix(path dir, string prefix)
987 {
988         auto iter = std::find_if(directory_iterator(dir), directory_iterator(), [prefix](path const& p) {
989                 return boost::starts_with(p.filename().string(), prefix);
990         });
991
992         BOOST_REQUIRE(iter != directory_iterator());
993         return iter->path();
994 }
995
996
997 path find_cpl (path dir)
998 {
999         return find_prefix(dir, "cpl_");
1000 }
1001
1002
1003 path
1004 find_pkl(path dir)
1005 {
1006         return find_prefix(dir, "pkl_");
1007 }
1008
1009
1010 path
1011 find_asset_map(path dir)
1012 {
1013         return find_prefix(dir, "ASSETMAP");
1014 }
1015
1016
1017 /* DCP with invalid CompositionMetadataAsset */
1018 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_bad_tag)
1019 {
1020         using namespace boost::filesystem;
1021
1022         path const dir("build/test/verify_invalid_cpl_metadata_bad_tag");
1023         prepare_directory (dir);
1024
1025         auto reel = make_shared<dcp::Reel>();
1026         reel->add (black_picture_asset(dir));
1027         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1028         cpl->add (reel);
1029         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1030         cpl->set_main_sound_sample_rate (48000);
1031         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1032         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1033         cpl->set_version_number (1);
1034
1035         reel->add (simple_markers());
1036
1037         dcp::DCP dcp (dir);
1038         dcp.add (cpl);
1039         dcp.set_annotation_text("hello");
1040         dcp.write_xml();
1041
1042         {
1043                 Editor e (find_cpl(dir));
1044                 e.replace ("MainSound", "MainSoundX");
1045         }
1046
1047         check_verify_result (
1048                 { dir },
1049                 {
1050                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXConfiguration'"), canonical(cpl->file().get()), 50 },
1051                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXSampleRate'"), canonical(cpl->file().get()), 51 },
1052                         {
1053                                 dcp::VerificationNote::Type::ERROR,
1054                                 dcp::VerificationNote::Code::INVALID_XML,
1055                                 string("element 'meta:MainSoundXConfiguration' is not allowed for content model "
1056                                        "'(Id,AnnotationText?,EditRate,IntrinsicDuration,EntryPoint?,Duration?,"
1057                                        "FullContentTitleText,ReleaseTerritory?,VersionNumber?,Chain?,Distributor?,"
1058                                        "Facility?,AlternateContentVersionList?,Luminance?,MainSoundConfiguration,"
1059                                        "MainSoundSampleRate,MainPictureStoredArea,MainPictureActiveArea,MainSubtitleLanguageList?,"
1060                                        "ExtensionMetadataList?,)'"),
1061                                 canonical(cpl->file().get()),
1062                                 71
1063                         },
1064                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) },
1065                 });
1066 }
1067
1068
1069 /* DCP with invalid CompositionMetadataAsset */
1070 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_missing_tag)
1071 {
1072         path const dir("build/test/verify_invalid_cpl_metadata_missing_tag");
1073         prepare_directory (dir);
1074
1075         auto reel = make_shared<dcp::Reel>();
1076         reel->add (black_picture_asset(dir));
1077         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1078         cpl->add (reel);
1079         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1080         cpl->set_main_sound_sample_rate (48000);
1081         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1082         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1083
1084         dcp::DCP dcp (dir);
1085         dcp.add (cpl);
1086         dcp.set_annotation_text("hello");
1087         dcp.write_xml();
1088
1089         {
1090                 Editor e (find_cpl(dir));
1091                 e.replace ("meta:Width", "meta:WidthX");
1092         }
1093
1094         check_verify_result (
1095                 { dir },
1096                 {{ dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::FAILED_READ, string("missing XML tag Width in MainPictureStoredArea") }}
1097                 );
1098 }
1099
1100
1101 BOOST_AUTO_TEST_CASE (verify_invalid_language1)
1102 {
1103         path const dir("build/test/verify_invalid_language1");
1104         prepare_directory (dir);
1105         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
1106         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
1107         asset->_language = "wrong-andbad";
1108         asset->write (dir / "subs.mxf");
1109         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
1110         reel_asset->_language = "badlang";
1111         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1112
1113         check_verify_result (
1114                 { dir },
1115                 {
1116                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
1117                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
1118                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1119                 });
1120 }
1121
1122
1123 /* SMPTE DCP with invalid <Language> in the MainClosedCaption reel and also in the XML within the MXF */
1124 BOOST_AUTO_TEST_CASE (verify_invalid_language2)
1125 {
1126         path const dir("build/test/verify_invalid_language2");
1127         prepare_directory (dir);
1128         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
1129         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
1130         asset->_language = "wrong-andbad";
1131         asset->write (dir / "subs.mxf");
1132         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
1133         reel_asset->_language = "badlang";
1134         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1135
1136         check_verify_result (
1137                 {dir},
1138                 {
1139                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
1140                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
1141                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1142                 });
1143 }
1144
1145
1146 /* SMPTE DCP with invalid <Language> in the MainSound reel, the CPL additional subtitles languages and
1147  * the release territory.
1148  */
1149 BOOST_AUTO_TEST_CASE (verify_invalid_language3)
1150 {
1151         path const dir("build/test/verify_invalid_language3");
1152         prepare_directory (dir);
1153
1154         auto picture = simple_picture (dir, "foo");
1155         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
1156         auto reel = make_shared<dcp::Reel>();
1157         reel->add (reel_picture);
1158         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "frobozz");
1159         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
1160         reel->add (reel_sound);
1161         reel->add (simple_markers());
1162
1163         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1164         cpl->add (reel);
1165         cpl->_additional_subtitle_languages.push_back("this-is-wrong");
1166         cpl->_additional_subtitle_languages.push_back("andso-is-this");
1167         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1168         cpl->set_main_sound_sample_rate (48000);
1169         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1170         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1171         cpl->set_version_number (1);
1172         cpl->_release_territory = "fred-jim";
1173         auto dcp = make_shared<dcp::DCP>(dir);
1174         dcp->add (cpl);
1175         dcp->set_annotation_text("hello");
1176         dcp->write_xml();
1177
1178         check_verify_result (
1179                 { dir },
1180                 {
1181                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("this-is-wrong") },
1182                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("andso-is-this") },
1183                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("fred-jim") },
1184                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("frobozz") },
1185                 });
1186 }
1187
1188
1189 static
1190 vector<dcp::VerificationNote>
1191 check_picture_size (int width, int height, int frame_rate, bool three_d)
1192 {
1193         using namespace boost::filesystem;
1194
1195         path dcp_path = "build/test/verify_picture_test";
1196         prepare_directory (dcp_path);
1197
1198         shared_ptr<dcp::PictureAsset> mp;
1199         if (three_d) {
1200                 mp = make_shared<dcp::StereoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1201         } else {
1202                 mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1203         }
1204         auto picture_writer = mp->start_write (dcp_path / "video.mxf", false);
1205
1206         auto image = black_image (dcp::Size(width, height));
1207         auto j2c = dcp::compress_j2k (image, 100000000, frame_rate, three_d, width > 2048);
1208         int const length = three_d ? frame_rate * 2 : frame_rate;
1209         for (int i = 0; i < length; ++i) {
1210                 picture_writer->write (j2c.data(), j2c.size());
1211         }
1212         picture_writer->finalize ();
1213
1214         auto d = make_shared<dcp::DCP>(dcp_path);
1215         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1216         cpl->set_annotation_text ("A Test DCP");
1217         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
1218         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1219         cpl->set_main_sound_sample_rate (48000);
1220         cpl->set_main_picture_stored_area(dcp::Size(width, height));
1221         cpl->set_main_picture_active_area(dcp::Size(width, height));
1222         cpl->set_version_number (1);
1223
1224         auto reel = make_shared<dcp::Reel>();
1225
1226         if (three_d) {
1227                 reel->add (make_shared<dcp::ReelStereoPictureAsset>(std::dynamic_pointer_cast<dcp::StereoPictureAsset>(mp), 0));
1228         } else {
1229                 reel->add (make_shared<dcp::ReelMonoPictureAsset>(std::dynamic_pointer_cast<dcp::MonoPictureAsset>(mp), 0));
1230         }
1231
1232         reel->add (simple_markers(frame_rate));
1233
1234         cpl->add (reel);
1235
1236         d->add (cpl);
1237         d->set_annotation_text("A Test DCP");
1238         d->write_xml();
1239
1240         return dcp::verify ({dcp_path}, &stage, &progress, xsd_test);
1241 }
1242
1243
1244 static
1245 void
1246 check_picture_size_ok (int width, int height, int frame_rate, bool three_d)
1247 {
1248         auto notes = check_picture_size(width, height, frame_rate, three_d);
1249         BOOST_CHECK_EQUAL (notes.size(), 0U);
1250 }
1251
1252
1253 static
1254 void
1255 check_picture_size_bad_frame_size (int width, int height, int frame_rate, bool three_d)
1256 {
1257         auto notes = check_picture_size(width, height, frame_rate, three_d);
1258         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1259         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1260         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS);
1261 }
1262
1263
1264 static
1265 void
1266 check_picture_size_bad_2k_frame_rate (int width, int height, int frame_rate, bool three_d)
1267 {
1268         auto notes = check_picture_size(width, height, frame_rate, three_d);
1269         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
1270         BOOST_CHECK_EQUAL (notes.back().type(), dcp::VerificationNote::Type::BV21_ERROR);
1271         BOOST_CHECK_EQUAL (notes.back().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K);
1272 }
1273
1274
1275 static
1276 void
1277 check_picture_size_bad_4k_frame_rate (int width, int height, int frame_rate, bool three_d)
1278 {
1279         auto notes = check_picture_size(width, height, frame_rate, three_d);
1280         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1281         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1282         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K);
1283 }
1284
1285
1286 BOOST_AUTO_TEST_CASE (verify_picture_size)
1287 {
1288         using namespace boost::filesystem;
1289
1290         /* 2K scope */
1291         check_picture_size_ok (2048, 858, 24, false);
1292         check_picture_size_ok (2048, 858, 25, false);
1293         check_picture_size_ok (2048, 858, 48, false);
1294         check_picture_size_ok (2048, 858, 24, true);
1295         check_picture_size_ok (2048, 858, 25, true);
1296         check_picture_size_ok (2048, 858, 48, true);
1297
1298         /* 2K flat */
1299         check_picture_size_ok (1998, 1080, 24, false);
1300         check_picture_size_ok (1998, 1080, 25, false);
1301         check_picture_size_ok (1998, 1080, 48, false);
1302         check_picture_size_ok (1998, 1080, 24, true);
1303         check_picture_size_ok (1998, 1080, 25, true);
1304         check_picture_size_ok (1998, 1080, 48, true);
1305
1306         /* 4K scope */
1307         check_picture_size_ok (4096, 1716, 24, false);
1308
1309         /* 4K flat */
1310         check_picture_size_ok (3996, 2160, 24, false);
1311
1312         /* Bad frame size */
1313         check_picture_size_bad_frame_size (2050, 858, 24, false);
1314         check_picture_size_bad_frame_size (2048, 658, 25, false);
1315         check_picture_size_bad_frame_size (1920, 1080, 48, true);
1316         check_picture_size_bad_frame_size (4000, 2000, 24, true);
1317
1318         /* Bad 2K frame rate */
1319         check_picture_size_bad_2k_frame_rate (2048, 858, 26, false);
1320         check_picture_size_bad_2k_frame_rate (2048, 858, 31, false);
1321         check_picture_size_bad_2k_frame_rate (1998, 1080, 50, true);
1322
1323         /* Bad 4K frame rate */
1324         check_picture_size_bad_4k_frame_rate (3996, 2160, 25, false);
1325         check_picture_size_bad_4k_frame_rate (3996, 2160, 48, false);
1326
1327         /* No 4K 3D */
1328         auto notes = check_picture_size(3996, 2160, 24, true);
1329         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1330         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1331         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D);
1332 }
1333
1334
1335 static
1336 void
1337 add_test_subtitle (shared_ptr<dcp::SubtitleAsset> asset, int start_frame, int end_frame, float v_position = 0, dcp::VAlign v_align = dcp::VAlign::CENTER, string text = "Hello")
1338 {
1339         asset->add (
1340                 std::make_shared<dcp::SubtitleString>(
1341                         optional<string>(),
1342                         false,
1343                         false,
1344                         false,
1345                         dcp::Colour(),
1346                         42,
1347                         1,
1348                         dcp::Time(start_frame, 24, 24),
1349                         dcp::Time(end_frame, 24, 24),
1350                         0,
1351                         dcp::HAlign::CENTER,
1352                         v_position,
1353                         v_align,
1354                         0,
1355                         dcp::Direction::LTR,
1356                         text,
1357                         dcp::Effect::NONE,
1358                         dcp::Colour(),
1359                         dcp::Time(),
1360                         dcp::Time(),
1361                         0
1362                 )
1363         );
1364 }
1365
1366
1367 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
1368 {
1369         path const dir("build/test/verify_invalid_closed_caption_xml_size_in_bytes");
1370         prepare_directory (dir);
1371
1372         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1373         for (int i = 0; i < 2048; ++i) {
1374                 add_test_subtitle (asset, i * 24, i * 24 + 20);
1375         }
1376         asset->set_language (dcp::LanguageTag("de-DE"));
1377         asset->write (dir / "subs.mxf");
1378         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
1379         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1380
1381         check_verify_result (
1382                 { dir },
1383                 {
1384                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1385                         {
1386                                 dcp::VerificationNote::Type::BV21_ERROR,
1387                                 dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES,
1388                                 string("419346"),
1389                                 canonical(dir / "subs.mxf")
1390                         },
1391                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1392                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1393                 });
1394 }
1395
1396
1397 static
1398 shared_ptr<dcp::SMPTESubtitleAsset>
1399 make_large_subtitle_asset (path font_file)
1400 {
1401         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1402         dcp::ArrayData big_fake_font(1024 * 1024);
1403         big_fake_font.write (font_file);
1404         for (int i = 0; i < 116; ++i) {
1405                 asset->add_font (dcp::String::compose("big%1", i), big_fake_font);
1406         }
1407         return asset;
1408 }
1409
1410
1411 template <class T>
1412 void
1413 verify_timed_text_asset_too_large (string name)
1414 {
1415         auto const dir = path("build/test") / name;
1416         prepare_directory (dir);
1417         auto asset = make_large_subtitle_asset (dir / "font.ttf");
1418         add_test_subtitle (asset, 0, 240);
1419         asset->set_language (dcp::LanguageTag("de-DE"));
1420         asset->write (dir / "subs.mxf");
1421
1422         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), 240, 0);
1423         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1424
1425         check_verify_result (
1426                 { dir },
1427                 {
1428                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121695542"), canonical(dir / "subs.mxf") },
1429                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, string("121634816"), canonical(dir / "subs.mxf") },
1430                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1431                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1432                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1433                 });
1434 }
1435
1436
1437 BOOST_AUTO_TEST_CASE (verify_subtitle_asset_too_large)
1438 {
1439         verify_timed_text_asset_too_large<dcp::ReelSMPTESubtitleAsset>("verify_subtitle_asset_too_large");
1440         verify_timed_text_asset_too_large<dcp::ReelSMPTEClosedCaptionAsset>("verify_closed_caption_asset_too_large");
1441 }
1442
1443
1444 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_language)
1445 {
1446         path dir = "build/test/verify_missing_subtitle_language";
1447         prepare_directory (dir);
1448         auto dcp = make_simple (dir, 1, 106);
1449
1450         string const xml =
1451                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1452                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1453                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1454                 "<ContentTitleText>Content</ContentTitleText>"
1455                 "<AnnotationText>Annotation</AnnotationText>"
1456                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1457                 "<ReelNumber>1</ReelNumber>"
1458                 "<EditRate>24 1</EditRate>"
1459                 "<TimeCodeRate>24</TimeCodeRate>"
1460                 "<StartTime>00:00:00:00</StartTime>"
1461                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1462                 "<SubtitleList>"
1463                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1464                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1465                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1466                 "</Subtitle>"
1467                 "</Font>"
1468                 "</SubtitleList>"
1469                 "</SubtitleReel>";
1470
1471         dcp::File xml_file(dir / "subs.xml", "w");
1472         BOOST_REQUIRE (xml_file);
1473         xml_file.write(xml.c_str(), xml.size(), 1);
1474         xml_file.close();
1475         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1476         subs->write (dir / "subs.mxf");
1477
1478         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1479         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1480         dcp->set_annotation_text("A Test DCP");
1481         dcp->write_xml();
1482
1483         check_verify_result (
1484                 { dir },
1485                 {
1486                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
1487                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1488                 });
1489 }
1490
1491
1492 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_languages)
1493 {
1494         path path ("build/test/verify_mismatched_subtitle_languages");
1495         auto constexpr reel_length = 192;
1496         auto dcp = make_simple (path, 2, reel_length);
1497         auto cpl = dcp->cpls()[0];
1498
1499         {
1500                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1501                 subs->set_language (dcp::LanguageTag("de-DE"));
1502                 subs->add (simple_subtitle());
1503                 subs->write (path / "subs1.mxf");
1504                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1505                 cpl->reels()[0]->add(reel_subs);
1506         }
1507
1508         {
1509                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1510                 subs->set_language (dcp::LanguageTag("en-US"));
1511                 subs->add (simple_subtitle());
1512                 subs->write (path / "subs2.mxf");
1513                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1514                 cpl->reels()[1]->add(reel_subs);
1515         }
1516
1517         dcp->set_annotation_text("A Test DCP");
1518         dcp->write_xml();
1519
1520         check_verify_result (
1521                 { path },
1522                 {
1523                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1524                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") },
1525                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES }
1526                 });
1527 }
1528
1529
1530 BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
1531 {
1532         path path ("build/test/verify_multiple_closed_caption_languages_allowed");
1533         auto constexpr reel_length = 192;
1534         auto dcp = make_simple (path, 2, reel_length);
1535         auto cpl = dcp->cpls()[0];
1536
1537         {
1538                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1539                 ccaps->set_language (dcp::LanguageTag("de-DE"));
1540                 ccaps->add (simple_subtitle());
1541                 ccaps->write (path / "subs1.mxf");
1542                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1543                 cpl->reels()[0]->add(reel_ccaps);
1544         }
1545
1546         {
1547                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1548                 ccaps->set_language (dcp::LanguageTag("en-US"));
1549                 ccaps->add (simple_subtitle());
1550                 ccaps->write (path / "subs2.mxf");
1551                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1552                 cpl->reels()[1]->add(reel_ccaps);
1553         }
1554
1555         dcp->set_annotation_text("A Test DCP");
1556         dcp->write_xml();
1557
1558         check_verify_result (
1559                 { path },
1560                 {
1561                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1562                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") }
1563                 });
1564 }
1565
1566
1567 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_start_time)
1568 {
1569         path dir = "build/test/verify_missing_subtitle_start_time";
1570         prepare_directory (dir);
1571         auto dcp = make_simple (dir, 1, 106);
1572
1573         string const xml =
1574                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1575                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1576                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1577                 "<ContentTitleText>Content</ContentTitleText>"
1578                 "<AnnotationText>Annotation</AnnotationText>"
1579                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1580                 "<ReelNumber>1</ReelNumber>"
1581                 "<Language>de-DE</Language>"
1582                 "<EditRate>24 1</EditRate>"
1583                 "<TimeCodeRate>24</TimeCodeRate>"
1584                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1585                 "<SubtitleList>"
1586                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1587                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1588                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1589                 "</Subtitle>"
1590                 "</Font>"
1591                 "</SubtitleList>"
1592                 "</SubtitleReel>";
1593
1594         dcp::File xml_file(dir / "subs.xml", "w");
1595         BOOST_REQUIRE (xml_file);
1596         xml_file.write(xml.c_str(), xml.size(), 1);
1597         xml_file.close();
1598         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1599         subs->write (dir / "subs.mxf");
1600
1601         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1602         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1603         dcp->set_annotation_text("A Test DCP");
1604         dcp->write_xml();
1605
1606         check_verify_result (
1607                 { dir },
1608                 {
1609                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1610                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1611                 });
1612 }
1613
1614
1615 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_start_time)
1616 {
1617         path dir = "build/test/verify_invalid_subtitle_start_time";
1618         prepare_directory (dir);
1619         auto dcp = make_simple (dir, 1, 106);
1620
1621         string const xml =
1622                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1623                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1624                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1625                 "<ContentTitleText>Content</ContentTitleText>"
1626                 "<AnnotationText>Annotation</AnnotationText>"
1627                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1628                 "<ReelNumber>1</ReelNumber>"
1629                 "<Language>de-DE</Language>"
1630                 "<EditRate>24 1</EditRate>"
1631                 "<TimeCodeRate>24</TimeCodeRate>"
1632                 "<StartTime>00:00:02:00</StartTime>"
1633                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1634                 "<SubtitleList>"
1635                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1636                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1637                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1638                 "</Subtitle>"
1639                 "</Font>"
1640                 "</SubtitleList>"
1641                 "</SubtitleReel>";
1642
1643         dcp::File xml_file(dir / "subs.xml", "w");
1644         BOOST_REQUIRE (xml_file);
1645         xml_file.write(xml.c_str(), xml.size(), 1);
1646         xml_file.close();
1647         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1648         subs->write (dir / "subs.mxf");
1649
1650         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1651         dcp->cpls().front()->reels().front()->add(reel_subs);
1652         dcp->set_annotation_text("A Test DCP");
1653         dcp->write_xml();
1654
1655         check_verify_result (
1656                 { dir },
1657                 {
1658                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1659                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1660                 });
1661 }
1662
1663
1664 class TestText
1665 {
1666 public:
1667         TestText (int in_, int out_, float v_position_ = 0, dcp::VAlign v_align_ = dcp::VAlign::CENTER, string text_ = "Hello")
1668                 : in(in_)
1669                 , out(out_)
1670                 , v_position(v_position_)
1671                 , v_align(v_align_)
1672                 , text(text_)
1673         {}
1674
1675         int in;
1676         int out;
1677         float v_position;
1678         dcp::VAlign v_align;
1679         string text;
1680 };
1681
1682
1683 template <class T>
1684 shared_ptr<dcp::CPL>
1685 dcp_with_text (path dir, vector<TestText> subs)
1686 {
1687         prepare_directory (dir);
1688         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1689         asset->set_start_time (dcp::Time());
1690         for (auto i: subs) {
1691                 add_test_subtitle (asset, i.in, i.out, i.v_position, i.v_align, i.text);
1692         }
1693         asset->set_language (dcp::LanguageTag("de-DE"));
1694         asset->write (dir / "subs.mxf");
1695
1696         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1697         return write_dcp_with_single_asset (dir, reel_asset);
1698 }
1699
1700
1701 template <class T>
1702 shared_ptr<dcp::CPL>
1703 dcp_with_text_from_file (path dir, boost::filesystem::path subs_xml)
1704 {
1705         prepare_directory (dir);
1706         auto asset = make_shared<dcp::SMPTESubtitleAsset>(subs_xml);
1707         asset->set_start_time (dcp::Time());
1708         asset->set_language (dcp::LanguageTag("de-DE"));
1709
1710         auto subs_mxf = dir / "subs.mxf";
1711         asset->write (subs_mxf);
1712
1713         /* The call to write() puts the asset into the DCP correctly but it will have
1714          * XML re-written by our parser.  Overwrite the MXF using the given file's verbatim
1715          * contents.
1716          */
1717         ASDCP::TimedText::MXFWriter writer;
1718         ASDCP::WriterInfo writer_info;
1719         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
1720         unsigned int c;
1721         Kumu::hex2bin (asset->id().c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
1722         DCP_ASSERT (c == Kumu::UUID_Length);
1723         ASDCP::TimedText::TimedTextDescriptor descriptor;
1724         descriptor.ContainerDuration = asset->intrinsic_duration();
1725         Kumu::hex2bin (asset->xml_id()->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
1726         DCP_ASSERT (c == Kumu::UUID_Length);
1727         ASDCP::Result_t r = writer.OpenWrite (subs_mxf.string().c_str(), writer_info, descriptor, 16384);
1728         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1729         r = writer.WriteTimedTextResource (dcp::file_to_string(subs_xml));
1730         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1731         writer.Finalize ();
1732
1733         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1734         return write_dcp_with_single_asset (dir, reel_asset);
1735 }
1736
1737
1738 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
1739 {
1740         auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
1741         /* Just too early */
1742         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
1743         check_verify_result (
1744                 { dir },
1745                 {
1746                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1747                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1748                 });
1749
1750 }
1751
1752
1753 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time)
1754 {
1755         auto const dir = path("build/test/verify_valid_subtitle_first_text_time");
1756         /* Just late enough */
1757         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
1758         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1759 }
1760
1761
1762 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time_on_second_reel)
1763 {
1764         auto const dir = path("build/test/verify_valid_subtitle_first_text_time_on_second_reel");
1765         prepare_directory (dir);
1766
1767         auto asset1 = make_shared<dcp::SMPTESubtitleAsset>();
1768         asset1->set_start_time (dcp::Time());
1769         /* Just late enough */
1770         add_test_subtitle (asset1, 4 * 24, 5 * 24);
1771         asset1->set_language (dcp::LanguageTag("de-DE"));
1772         asset1->write (dir / "subs1.mxf");
1773         auto reel_asset1 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset1, dcp::Fraction(24, 1), 5 * 24, 0);
1774         auto reel1 = make_shared<dcp::Reel>();
1775         reel1->add (reel_asset1);
1776         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 5 * 24);
1777         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
1778         reel1->add (markers1);
1779
1780         auto asset2 = make_shared<dcp::SMPTESubtitleAsset>();
1781         asset2->set_start_time (dcp::Time());
1782         /* This would be too early on first reel but should be OK on the second */
1783         add_test_subtitle (asset2, 3, 4 * 24);
1784         asset2->set_language (dcp::LanguageTag("de-DE"));
1785         asset2->write (dir / "subs2.mxf");
1786         auto reel_asset2 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset2, dcp::Fraction(24, 1), 4 * 24, 0);
1787         auto reel2 = make_shared<dcp::Reel>();
1788         reel2->add (reel_asset2);
1789         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 4 * 24);
1790         markers2->set (dcp::Marker::LFOC, dcp::Time(4 * 24 - 1, 24, 24));
1791         reel2->add (markers2);
1792
1793         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1794         cpl->add (reel1);
1795         cpl->add (reel2);
1796         auto dcp = make_shared<dcp::DCP>(dir);
1797         dcp->add (cpl);
1798         dcp->set_annotation_text("hello");
1799         dcp->write_xml();
1800
1801         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1802 }
1803
1804
1805 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_spacing)
1806 {
1807         auto const dir = path("build/test/verify_invalid_subtitle_spacing");
1808         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1809                 dir,
1810                 {
1811                         { 4 * 24,     5 * 24 },
1812                         { 5 * 24 + 1, 6 * 24 },
1813                 });
1814         check_verify_result (
1815                 {dir},
1816                 {
1817                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING },
1818                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1819                 });
1820 }
1821
1822
1823 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_spacing)
1824 {
1825         auto const dir = path("build/test/verify_valid_subtitle_spacing");
1826         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1827                 dir,
1828                 {
1829                         { 4 * 24,      5 * 24 },
1830                         { 5 * 24 + 16, 8 * 24 },
1831                 });
1832         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1833 }
1834
1835
1836 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_duration)
1837 {
1838         auto const dir = path("build/test/verify_invalid_subtitle_duration");
1839         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
1840         check_verify_result (
1841                 {dir},
1842                 {
1843                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION },
1844                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1845                 });
1846 }
1847
1848
1849 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_duration)
1850 {
1851         auto const dir = path("build/test/verify_valid_subtitle_duration");
1852         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
1853         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1854 }
1855
1856
1857 BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
1858 {
1859         auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
1860         prepare_directory (dir);
1861         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1862         asset->set_start_time (dcp::Time());
1863         add_test_subtitle (asset, 0, 4 * 24);
1864         asset->set_language (dcp::LanguageTag("de-DE"));
1865         asset->write (dir / "subs.mxf");
1866
1867         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
1868         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1869         check_verify_result (
1870                 {dir},
1871                 {
1872                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "72 96", boost::filesystem::canonical(asset->file().get()) },
1873                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1874                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
1875                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1876                 });
1877
1878 }
1879
1880
1881 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
1882 {
1883         auto const dir = path ("build/test/invalid_subtitle_line_count1");
1884         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1885                 dir,
1886                 {
1887                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1888                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1889                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1890                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1891                 });
1892         check_verify_result (
1893                 {dir},
1894                 {
1895                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1896                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1897                 });
1898 }
1899
1900
1901 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
1902 {
1903         auto const dir = path ("build/test/verify_valid_subtitle_line_count1");
1904         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1905                 dir,
1906                 {
1907                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1908                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1909                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1910                 });
1911         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1912 }
1913
1914
1915 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
1916 {
1917         auto const dir = path ("build/test/verify_invalid_subtitle_line_count2");
1918         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1919                 dir,
1920                 {
1921                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1922                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1923                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1924                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
1925                 });
1926         check_verify_result (
1927                 {dir},
1928                 {
1929                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1930                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1931                 });
1932 }
1933
1934
1935 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
1936 {
1937         auto const dir = path ("build/test/verify_valid_subtitle_line_count2");
1938         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1939                 dir,
1940                 {
1941                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1942                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1943                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1944                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
1945                 });
1946         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1947 }
1948
1949
1950 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
1951 {
1952         auto const dir = path ("build/test/verify_invalid_subtitle_line_length1");
1953         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1954                 dir,
1955                 {
1956                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123" }
1957                 });
1958         check_verify_result (
1959                 {dir},
1960                 {
1961                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH },
1962                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1963                 });
1964 }
1965
1966
1967 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
1968 {
1969         auto const dir = path ("build/test/verify_invalid_subtitle_line_length2");
1970         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1971                 dir,
1972                 {
1973                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
1974                 });
1975         check_verify_result (
1976                 {dir},
1977                 {
1978                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH },
1979                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1980                 });
1981 }
1982
1983
1984 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
1985 {
1986         auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
1987         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1988                 dir,
1989                 {
1990                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1991                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1992                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1993                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1994                 });
1995         check_verify_result (
1996                 {dir},
1997                 {
1998                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1999                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2000                 });
2001 }
2002
2003
2004 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
2005 {
2006         auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
2007         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2008                 dir,
2009                 {
2010                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
2011                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
2012                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
2013                 });
2014         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2015 }
2016
2017
2018 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
2019 {
2020         auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
2021         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2022                 dir,
2023                 {
2024                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
2025                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
2026                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
2027                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
2028                 });
2029         check_verify_result (
2030                 {dir},
2031                 {
2032                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
2033                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2034                 });
2035 }
2036
2037
2038 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
2039 {
2040         auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
2041         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2042                 dir,
2043                 {
2044                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
2045                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
2046                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
2047                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
2048                 });
2049         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2050 }
2051
2052
2053 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_length)
2054 {
2055         auto const dir = path ("build/test/verify_valid_closed_caption_line_length");
2056         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2057                 dir,
2058                 {
2059                         { 96, 300, 0.0, dcp::VAlign::CENTER, "01234567890123456789012345678901" }
2060                 });
2061         check_verify_result (
2062                 {dir},
2063                 {
2064                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2065                 });
2066 }
2067
2068
2069 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
2070 {
2071         auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
2072         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2073                 dir,
2074                 {
2075                         { 96, 300, 0.0, dcp::VAlign::CENTER, "0123456789012345678901234567890123" }
2076                 });
2077         check_verify_result (
2078                 {dir},
2079                 {
2080                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH },
2081                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2082                 });
2083 }
2084
2085
2086 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign1)
2087 {
2088         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign1");
2089         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2090                 dir,
2091                 {
2092                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2093                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2094                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2095                 });
2096         check_verify_result (
2097                 {dir},
2098                 {
2099                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2100                 });
2101 }
2102
2103
2104 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign2)
2105 {
2106         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign2");
2107         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2108                 dir,
2109                 {
2110                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2111                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2112                         { 96, 300, 0.2, dcp::VAlign::CENTER, "not fine" },
2113                 });
2114         check_verify_result (
2115                 {dir},
2116                 {
2117                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN },
2118                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2119                 });
2120 }
2121
2122
2123 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering1)
2124 {
2125         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering1");
2126         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2127                 dir,
2128                 {
2129                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2130                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2131                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2132                 });
2133         check_verify_result (
2134                 {dir},
2135                 {
2136                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2137                 });
2138 }
2139
2140
2141 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering2)
2142 {
2143         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering2");
2144         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2145                 dir,
2146                 {
2147                         { 96, 300, 0.2, dcp::VAlign::BOTTOM, "This" },
2148                         { 96, 300, 0.1, dcp::VAlign::BOTTOM, "is" },
2149                         { 96, 300, 0.0, dcp::VAlign::BOTTOM, "also fine" },
2150                 });
2151         check_verify_result (
2152                 {dir},
2153                 {
2154                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2155                 });
2156 }
2157
2158
2159 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering3)
2160 {
2161         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering3");
2162         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering3.xml");
2163         check_verify_result (
2164                 {dir},
2165                 {
2166                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING },
2167                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2168                 });
2169 }
2170
2171
2172 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering4)
2173 {
2174         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering4");
2175         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering4.xml");
2176         check_verify_result (
2177                 {dir},
2178                 {
2179                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2180                 });
2181 }
2182
2183
2184
2185 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
2186 {
2187         path const dir("build/test/verify_invalid_sound_frame_rate");
2188         prepare_directory (dir);
2189
2190         auto picture = simple_picture (dir, "foo");
2191         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
2192         auto reel = make_shared<dcp::Reel>();
2193         reel->add (reel_picture);
2194         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000, boost::none);
2195         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
2196         reel->add (reel_sound);
2197         reel->add (simple_markers());
2198         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2199         cpl->add (reel);
2200         auto dcp = make_shared<dcp::DCP>(dir);
2201         dcp->add (cpl);
2202         dcp->set_annotation_text("hello");
2203         dcp->write_xml();
2204
2205         check_verify_result (
2206                 {dir},
2207                 {
2208                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE, string("96000"), canonical(dir / "audiofoo.mxf") },
2209                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2210                 });
2211 }
2212
2213
2214 BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
2215 {
2216         path const dir("build/test/verify_missing_cpl_annotation_text");
2217         auto dcp = make_simple (dir);
2218         dcp->set_annotation_text("A Test DCP");
2219         dcp->write_xml();
2220
2221         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2222
2223         auto const cpl = dcp->cpls()[0];
2224
2225         {
2226                 BOOST_REQUIRE (cpl->file());
2227                 Editor e(cpl->file().get());
2228                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "");
2229         }
2230
2231         check_verify_result (
2232                 {dir},
2233                 {
2234                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2235                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2236                 });
2237 }
2238
2239
2240 BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
2241 {
2242         path const dir("build/test/verify_mismatched_cpl_annotation_text");
2243         auto dcp = make_simple (dir);
2244         dcp->set_annotation_text("A Test DCP");
2245         dcp->write_xml();
2246
2247         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2248         auto const cpl = dcp->cpls()[0];
2249
2250         {
2251                 BOOST_REQUIRE (cpl->file());
2252                 Editor e(cpl->file().get());
2253                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "<AnnotationText>A Test DCP 1</AnnotationText>");
2254         }
2255
2256         check_verify_result (
2257                 {dir},
2258                 {
2259                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2260                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2261                 });
2262 }
2263
2264
2265 BOOST_AUTO_TEST_CASE (verify_mismatched_asset_duration)
2266 {
2267         path const dir("build/test/verify_mismatched_asset_duration");
2268         prepare_directory (dir);
2269         shared_ptr<dcp::DCP> dcp (new dcp::DCP(dir));
2270         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2271
2272         shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (dir, "", 24);
2273         shared_ptr<dcp::SoundAsset> ms = simple_sound (dir, "", dcp::MXFMetadata(), "en-US", 25);
2274
2275         auto reel = make_shared<dcp::Reel>(
2276                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2277                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2278                 );
2279
2280         reel->add (simple_markers());
2281         cpl->add (reel);
2282
2283         dcp->add (cpl);
2284         dcp->set_annotation_text("A Test DCP");
2285         dcp->write_xml();
2286
2287         check_verify_result (
2288                 {dir},
2289                 {
2290                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION },
2291                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), canonical(cpl->file().get()) }
2292                 });
2293 }
2294
2295
2296
2297 static
2298 shared_ptr<dcp::CPL>
2299 verify_subtitles_must_be_in_all_reels_check (path dir, bool add_to_reel1, bool add_to_reel2)
2300 {
2301         prepare_directory (dir);
2302         auto dcp = make_shared<dcp::DCP>(dir);
2303         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2304
2305         auto constexpr reel_length = 192;
2306
2307         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2308         subs->set_language (dcp::LanguageTag("de-DE"));
2309         subs->set_start_time (dcp::Time());
2310         subs->add (simple_subtitle());
2311         subs->write (dir / "subs.mxf");
2312         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
2313
2314         auto reel1 = make_shared<dcp::Reel>(
2315                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2316                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2317                 );
2318
2319         if (add_to_reel1) {
2320                 reel1->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2321         }
2322
2323         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2324         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2325         reel1->add (markers1);
2326
2327         cpl->add (reel1);
2328
2329         auto reel2 = make_shared<dcp::Reel>(
2330                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2331                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2332                 );
2333
2334         if (add_to_reel2) {
2335                 reel2->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2336         }
2337
2338         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2339         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2340         reel2->add (markers2);
2341
2342         cpl->add (reel2);
2343
2344         dcp->add (cpl);
2345         dcp->set_annotation_text("A Test DCP");
2346         dcp->write_xml();
2347
2348         return cpl;
2349 }
2350
2351
2352 BOOST_AUTO_TEST_CASE (verify_missing_main_subtitle_from_some_reels)
2353 {
2354         {
2355                 path dir ("build/test/missing_main_subtitle_from_some_reels");
2356                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, false);
2357                 check_verify_result (
2358                         { dir },
2359                         {
2360                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS },
2361                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2362                         });
2363
2364         }
2365
2366         {
2367                 path dir ("build/test/verify_subtitles_must_be_in_all_reels2");
2368                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, true);
2369                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2370         }
2371
2372         {
2373                 path dir ("build/test/verify_subtitles_must_be_in_all_reels1");
2374                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, false, false);
2375                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2376         }
2377 }
2378
2379
2380 static
2381 shared_ptr<dcp::CPL>
2382 verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1, int caps_in_reel2)
2383 {
2384         prepare_directory (dir);
2385         auto dcp = make_shared<dcp::DCP>(dir);
2386         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2387
2388         auto constexpr reel_length = 192;
2389
2390         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2391         subs->set_language (dcp::LanguageTag("de-DE"));
2392         subs->set_start_time (dcp::Time());
2393         subs->add (simple_subtitle());
2394         subs->write (dir / "subs.mxf");
2395
2396         auto reel1 = make_shared<dcp::Reel>(
2397                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2398                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2399                 );
2400
2401         for (int i = 0; i < caps_in_reel1; ++i) {
2402                 reel1->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2403         }
2404
2405         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2406         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2407         reel1->add (markers1);
2408
2409         cpl->add (reel1);
2410
2411         auto reel2 = make_shared<dcp::Reel>(
2412                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2413                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2414                 );
2415
2416         for (int i = 0; i < caps_in_reel2; ++i) {
2417                 reel2->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2418         }
2419
2420         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2421         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2422         reel2->add (markers2);
2423
2424         cpl->add (reel2);
2425
2426         dcp->add (cpl);
2427         dcp->set_annotation_text("A Test DCP");
2428         dcp->write_xml();
2429
2430         return cpl;
2431 }
2432
2433
2434 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_asset_counts)
2435 {
2436         {
2437                 path dir ("build/test/mismatched_closed_caption_asset_counts");
2438                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 3, 4);
2439                 check_verify_result (
2440                         {dir},
2441                         {
2442                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS },
2443                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2444                         });
2445         }
2446
2447         {
2448                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels2");
2449                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 4, 4);
2450                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2451         }
2452
2453         {
2454                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels3");
2455                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 0, 0);
2456                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2457         }
2458 }
2459
2460
2461 template <class T>
2462 void
2463 verify_text_entry_point_check (path dir, dcp::VerificationNote::Code code, boost::function<void (shared_ptr<T>)> adjust)
2464 {
2465         prepare_directory (dir);
2466         auto dcp = make_shared<dcp::DCP>(dir);
2467         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2468
2469         auto constexpr reel_length = 192;
2470
2471         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2472         subs->set_language (dcp::LanguageTag("de-DE"));
2473         subs->set_start_time (dcp::Time());
2474         subs->add (simple_subtitle());
2475         subs->write (dir / "subs.mxf");
2476         auto reel_text = make_shared<T>(subs, dcp::Fraction(24, 1), reel_length, 0);
2477         adjust (reel_text);
2478
2479         auto reel = make_shared<dcp::Reel>(
2480                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2481                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2482                 );
2483
2484         reel->add (reel_text);
2485
2486         reel->add (simple_markers(reel_length));
2487
2488         cpl->add (reel);
2489
2490         dcp->add (cpl);
2491         dcp->set_annotation_text("A Test DCP");
2492         dcp->write_xml();
2493
2494         check_verify_result (
2495                 {dir},
2496                 {
2497                         { dcp::VerificationNote::Type::BV21_ERROR, code, subs->id() },
2498                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2499                 });
2500 }
2501
2502
2503 BOOST_AUTO_TEST_CASE (verify_text_entry_point)
2504 {
2505         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2506                 "build/test/verify_subtitle_entry_point_must_be_present",
2507                 dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT,
2508                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2509                         asset->unset_entry_point ();
2510                         }
2511                 );
2512
2513         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2514                 "build/test/verify_subtitle_entry_point_must_be_zero",
2515                 dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT,
2516                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2517                         asset->set_entry_point (4);
2518                         }
2519                 );
2520
2521         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2522                 "build/test/verify_closed_caption_entry_point_must_be_present",
2523                 dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
2524                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2525                         asset->unset_entry_point ();
2526                         }
2527                 );
2528
2529         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2530                 "build/test/verify_closed_caption_entry_point_must_be_zero",
2531                 dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
2532                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2533                         asset->set_entry_point (9);
2534                         }
2535                 );
2536 }
2537
2538
2539 BOOST_AUTO_TEST_CASE (verify_missing_hash)
2540 {
2541         RNGFixer fix;
2542
2543         path const dir("build/test/verify_missing_hash");
2544         auto dcp = make_simple (dir);
2545         dcp->set_annotation_text("A Test DCP");
2546         dcp->write_xml();
2547
2548         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2549         auto const cpl = dcp->cpls()[0];
2550         BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
2551         BOOST_REQUIRE (cpl->reels()[0]->main_picture());
2552         auto asset_id = cpl->reels()[0]->main_picture()->id();
2553
2554         {
2555                 BOOST_REQUIRE (cpl->file());
2556                 Editor e(cpl->file().get());
2557                 e.delete_first_line_containing("<Hash>");
2558         }
2559
2560         check_verify_result (
2561                 {dir},
2562                 {
2563                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2564                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
2565                 });
2566 }
2567
2568
2569 static
2570 void
2571 verify_markers_test (
2572         path dir,
2573         vector<pair<dcp::Marker, dcp::Time>> markers,
2574         vector<dcp::VerificationNote> test_notes
2575         )
2576 {
2577         auto dcp = make_simple (dir);
2578         dcp->cpls()[0]->set_content_kind (dcp::ContentKind::FEATURE);
2579         auto markers_asset = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 24);
2580         for (auto const& i: markers) {
2581                 markers_asset->set (i.first, i.second);
2582         }
2583         dcp->cpls()[0]->reels()[0]->add(markers_asset);
2584         dcp->set_annotation_text("A Test DCP");
2585         dcp->write_xml();
2586
2587         check_verify_result ({dir}, test_notes);
2588 }
2589
2590
2591 BOOST_AUTO_TEST_CASE (verify_markers)
2592 {
2593         verify_markers_test (
2594                 "build/test/verify_markers_all_correct",
2595                 {
2596                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2597                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2598                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2599                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2600                 },
2601                 {}
2602                 );
2603
2604         verify_markers_test (
2605                 "build/test/verify_markers_missing_ffec",
2606                 {
2607                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2608                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2609                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2610                 },
2611                 {
2612                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE }
2613                 });
2614
2615         verify_markers_test (
2616                 "build/test/verify_markers_missing_ffmc",
2617                 {
2618                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2619                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2620                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2621                 },
2622                 {
2623                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
2624                 });
2625
2626         verify_markers_test (
2627                 "build/test/verify_markers_missing_ffoc",
2628                 {
2629                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2630                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2631                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2632                 },
2633                 {
2634                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC}
2635                 });
2636
2637         verify_markers_test (
2638                 "build/test/verify_markers_missing_lfoc",
2639                 {
2640                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2641                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2642                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) }
2643                 },
2644                 {
2645                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC }
2646                 });
2647
2648         verify_markers_test (
2649                 "build/test/verify_markers_incorrect_ffoc",
2650                 {
2651                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2652                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2653                         { dcp::Marker::FFOC, dcp::Time(3, 24, 24) },
2654                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2655                 },
2656                 {
2657                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_FFOC, string("3") }
2658                 });
2659
2660         verify_markers_test (
2661                 "build/test/verify_markers_incorrect_lfoc",
2662                 {
2663                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2664                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2665                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2666                         { dcp::Marker::LFOC, dcp::Time(18, 24, 24) }
2667                 },
2668                 {
2669                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_LFOC, string("18") }
2670                 });
2671 }
2672
2673
2674 BOOST_AUTO_TEST_CASE (verify_missing_cpl_metadata_version_number)
2675 {
2676         path dir = "build/test/verify_missing_cpl_metadata_version_number";
2677         prepare_directory (dir);
2678         auto dcp = make_simple (dir);
2679         auto cpl = dcp->cpls()[0];
2680         cpl->unset_version_number();
2681         dcp->set_annotation_text("A Test DCP");
2682         dcp->write_xml();
2683
2684         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get() }});
2685 }
2686
2687
2688 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
2689 {
2690         path dir = "build/test/verify_missing_extension_metadata1";
2691         auto dcp = make_simple (dir);
2692         dcp->set_annotation_text("A Test DCP");
2693         dcp->write_xml();
2694
2695         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2696         auto cpl = dcp->cpls()[0];
2697
2698         {
2699                 Editor e (cpl->file().get());
2700                 e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
2701         }
2702
2703         check_verify_result (
2704                 {dir},
2705                 {
2706                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2707                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2708                 });
2709 }
2710
2711
2712 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
2713 {
2714         path dir = "build/test/verify_missing_extension_metadata2";
2715         auto dcp = make_simple (dir);
2716         dcp->set_annotation_text("A Test DCP");
2717         dcp->write_xml();
2718
2719         auto cpl = dcp->cpls()[0];
2720
2721         {
2722                 Editor e (cpl->file().get());
2723                 e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
2724         }
2725
2726         check_verify_result (
2727                 {dir},
2728                 {
2729                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2730                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2731                 });
2732 }
2733
2734
2735 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
2736 {
2737         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata3";
2738         auto dcp = make_simple (dir);
2739         dcp->set_annotation_text("A Test DCP");
2740         dcp->write_xml();
2741
2742         auto const cpl = dcp->cpls()[0];
2743
2744         {
2745                 Editor e (cpl->file().get());
2746                 e.replace ("<meta:Name>A", "<meta:NameX>A");
2747                 e.replace ("n</meta:Name>", "n</meta:NameX>");
2748         }
2749
2750         check_verify_result (
2751                 {dir},
2752                 {
2753                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:NameX'"), cpl->file().get(), 70 },
2754                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:NameX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 77 },
2755                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2756                 });
2757 }
2758
2759
2760 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
2761 {
2762         path dir = "build/test/verify_invalid_extension_metadata1";
2763         auto dcp = make_simple (dir);
2764         dcp->set_annotation_text("A Test DCP");
2765         dcp->write_xml();
2766
2767         auto cpl = dcp->cpls()[0];
2768
2769         {
2770                 Editor e (cpl->file().get());
2771                 e.replace ("Application", "Fred");
2772         }
2773
2774         check_verify_result (
2775                 {dir},
2776                 {
2777                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2778                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
2779                 });
2780 }
2781
2782
2783 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
2784 {
2785         path dir = "build/test/verify_invalid_extension_metadata2";
2786         auto dcp = make_simple (dir);
2787         dcp->set_annotation_text("A Test DCP");
2788         dcp->write_xml();
2789
2790         auto cpl = dcp->cpls()[0];
2791
2792         {
2793                 Editor e (cpl->file().get());
2794                 e.replace ("DCP Constraints Profile", "Fred");
2795         }
2796
2797         check_verify_result (
2798                 {dir},
2799                 {
2800                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2801                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
2802                 });
2803 }
2804
2805
2806 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
2807 {
2808         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata6";
2809         auto dcp = make_simple (dir);
2810         dcp->set_annotation_text("A Test DCP");
2811         dcp->write_xml();
2812
2813         auto const cpl = dcp->cpls()[0];
2814
2815         {
2816                 Editor e (cpl->file().get());
2817                 e.replace ("<meta:Value>", "<meta:ValueX>");
2818                 e.replace ("</meta:Value>", "</meta:ValueX>");
2819         }
2820
2821         check_verify_result (
2822                 {dir},
2823                 {
2824                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 74 },
2825                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:ValueX' is not allowed for content model '(Name,Value)'"), cpl->file().get(), 75 },
2826                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2827                 });
2828 }
2829
2830
2831 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
2832 {
2833         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata7";
2834         auto dcp = make_simple (dir);
2835         dcp->set_annotation_text("A Test DCP");
2836         dcp->write_xml();
2837
2838         auto const cpl = dcp->cpls()[0];
2839
2840         {
2841                 Editor e (cpl->file().get());
2842                 e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
2843         }
2844
2845         check_verify_result (
2846                 {dir},
2847                 {
2848                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2849                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Value> property should be 'SMPTE-RDD-52:2020-Bv2.1'"), cpl->file().get() },
2850                 });
2851 }
2852
2853
2854 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
2855 {
2856         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata8";
2857         auto dcp = make_simple (dir);
2858         dcp->set_annotation_text("A Test DCP");
2859         dcp->write_xml();
2860
2861         auto const cpl = dcp->cpls()[0];
2862
2863         {
2864                 Editor e (cpl->file().get());
2865                 e.replace ("<meta:Property>", "<meta:PropertyX>");
2866                 e.replace ("</meta:Property>", "</meta:PropertyX>");
2867         }
2868
2869         check_verify_result (
2870                 {dir},
2871                 {
2872                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 72 },
2873                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 76 },
2874                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2875                 });
2876 }
2877
2878
2879 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
2880 {
2881         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata9";
2882         auto dcp = make_simple (dir);
2883         dcp->set_annotation_text("A Test DCP");
2884         dcp->write_xml();
2885
2886         auto const cpl = dcp->cpls()[0];
2887
2888         {
2889                 Editor e (cpl->file().get());
2890                 e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
2891                 e.replace ("</meta:PropertyList>", "</meta:PropertyListX>");
2892         }
2893
2894         check_verify_result (
2895                 {dir},
2896                 {
2897                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 71 },
2898                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyListX' is not allowed for content model '(Name,PropertyList?,)'"), cpl->file().get(), 77 },
2899                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2900                 });
2901 }
2902
2903
2904
2905 BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
2906 {
2907         path dir = "build/test/verify_unsigned_cpl_with_encrypted_content";
2908         prepare_directory (dir);
2909         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2910                 copy_file (i.path(), dir / i.path().filename());
2911         }
2912
2913         path const pkl = dir / ( "pkl_" + encryption_test_pkl_id + ".xml" );
2914         path const cpl = dir / ( "cpl_" + encryption_test_cpl_id + ".xml");
2915
2916         {
2917                 Editor e (cpl);
2918                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2919         }
2920
2921         check_verify_result (
2922                 {dir},
2923                 {
2924                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id, canonical(cpl) },
2925                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl), },
2926                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2927                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2928                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2929                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2930                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2931                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, encryption_test_cpl_id, canonical(cpl) }
2932                 });
2933 }
2934
2935
2936 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_encrypted_content)
2937 {
2938         path dir = "build/test/unsigned_pkl_with_encrypted_content";
2939         prepare_directory (dir);
2940         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2941                 copy_file (i.path(), dir / i.path().filename());
2942         }
2943
2944         path const cpl = dir / ("cpl_" + encryption_test_cpl_id + ".xml");
2945         path const pkl = dir / ("pkl_" + encryption_test_pkl_id + ".xml");
2946         {
2947                 Editor e (pkl);
2948                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2949         }
2950
2951         check_verify_result (
2952                 {dir},
2953                 {
2954                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl) },
2955                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2956                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2957                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2958                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2959                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2960                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, encryption_test_pkl_id, canonical(pkl) },
2961                 });
2962 }
2963
2964
2965 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_unencrypted_content)
2966 {
2967         path dir = "build/test/verify_unsigned_pkl_with_unencrypted_content";
2968         prepare_directory (dir);
2969         for (auto i: directory_iterator("test/ref/DCP/dcp_test1")) {
2970                 copy_file (i.path(), dir / i.path().filename());
2971         }
2972
2973         {
2974                 Editor e (dir / dcp_test1_pkl);
2975                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2976         }
2977
2978         check_verify_result ({dir}, {});
2979 }
2980
2981
2982 BOOST_AUTO_TEST_CASE (verify_partially_encrypted)
2983 {
2984         path dir ("build/test/verify_must_not_be_partially_encrypted");
2985         prepare_directory (dir);
2986
2987         dcp::DCP d (dir);
2988
2989         auto signer = make_shared<dcp::CertificateChain>();
2990         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem")));
2991         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem")));
2992         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem")));
2993         signer->set_key (dcp::file_to_string("test/ref/crypt/leaf.key"));
2994
2995         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2996
2997         dcp::Key key;
2998
2999         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
3000         mp->set_key (key);
3001
3002         auto writer = mp->start_write (dir / "video.mxf", false);
3003         dcp::ArrayData j2c ("test/data/flat_red.j2c");
3004         for (int i = 0; i < 24; ++i) {
3005                 writer->write (j2c.data(), j2c.size());
3006         }
3007         writer->finalize ();
3008
3009         auto ms = simple_sound (dir, "", dcp::MXFMetadata(), "de-DE");
3010
3011         auto reel = make_shared<dcp::Reel>(
3012                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3013                 make_shared<dcp::ReelSoundAsset>(ms, 0)
3014                 );
3015
3016         reel->add (simple_markers());
3017
3018         cpl->add (reel);
3019
3020         cpl->set_content_version (
3021                 {"urn:uri:81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00", "81fb54df-e1bf-4647-8788-ea7ba154375b_2012-07-17T04:45:18+00:00"}
3022                 );
3023         cpl->set_annotation_text ("A Test DCP");
3024         cpl->set_issuer ("OpenDCP 0.0.25");
3025         cpl->set_creator ("OpenDCP 0.0.25");
3026         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
3027         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
3028         cpl->set_main_sound_sample_rate (48000);
3029         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
3030         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
3031         cpl->set_version_number (1);
3032
3033         d.add (cpl);
3034
3035         d.set_issuer("OpenDCP 0.0.25");
3036         d.set_creator("OpenDCP 0.0.25");
3037         d.set_issue_date("2012-07-17T04:45:18+00:00");
3038         d.set_annotation_text("A Test DCP");
3039         d.write_xml(signer);
3040
3041         check_verify_result (
3042                 {dir},
3043                 {
3044                         {dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED},
3045                 });
3046 }
3047
3048
3049 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_2k)
3050 {
3051         vector<dcp::VerificationNote> notes;
3052         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "JourneyToJah_TLR-1_F_EN-DE-FR_CH_51_2K_LOK_20140225_DGL_SMPTE_OV", "j2c.mxf"));
3053         auto reader = picture.start_read ();
3054         auto frame = reader->get_frame (0);
3055         verify_j2k (frame, notes);
3056         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3057 }
3058
3059
3060 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_4k)
3061 {
3062         vector<dcp::VerificationNote> notes;
3063         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "sul", "TLR"));
3064         auto reader = picture.start_read ();
3065         auto frame = reader->get_frame (0);
3066         verify_j2k (frame, notes);
3067         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3068 }
3069
3070
3071 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_libdcp)
3072 {
3073         boost::filesystem::path dir = "build/test/verify_jpeg2000_codestream_libdcp";
3074         prepare_directory (dir);
3075         auto dcp = make_simple (dir);
3076         dcp->write_xml ();
3077         vector<dcp::VerificationNote> notes;
3078         dcp::MonoPictureAsset picture (find_file(dir, "video"));
3079         auto reader = picture.start_read ();
3080         auto frame = reader->get_frame (0);
3081         verify_j2k (frame, notes);
3082         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3083 }
3084
3085
3086 /** Check that ResourceID and the XML ID being different is spotted */
3087 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_resource_id)
3088 {
3089         boost::filesystem::path const dir = "build/test/verify_mismatched_subtitle_resource_id";
3090         prepare_directory (dir);
3091
3092         ASDCP::WriterInfo writer_info;
3093         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3094
3095         unsigned int c;
3096         auto mxf_id = dcp::make_uuid ();
3097         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3098         BOOST_REQUIRE (c == Kumu::UUID_Length);
3099
3100         auto resource_id = dcp::make_uuid ();
3101         ASDCP::TimedText::TimedTextDescriptor descriptor;
3102         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3103         DCP_ASSERT (c == Kumu::UUID_Length);
3104
3105         auto xml_id = dcp::make_uuid ();
3106         ASDCP::TimedText::MXFWriter writer;
3107         auto subs_mxf = dir / "subs.mxf";
3108         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3109         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3110         writer.WriteTimedTextResource (dcp::String::compose(
3111                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3112                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3113                 "<Id>urn:uuid:%1</Id>"
3114                 "<ContentTitleText>Content</ContentTitleText>"
3115                 "<AnnotationText>Annotation</AnnotationText>"
3116                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3117                 "<ReelNumber>1</ReelNumber>"
3118                 "<Language>en-US</Language>"
3119                 "<EditRate>25 1</EditRate>"
3120                 "<TimeCodeRate>25</TimeCodeRate>"
3121                 "<StartTime>00:00:00:00</StartTime>"
3122                 "<SubtitleList>"
3123                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3124                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3125                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3126                 "</Subtitle>"
3127                 "</Font>"
3128                 "</SubtitleList>"
3129                 "</SubtitleReel>",
3130                 xml_id).c_str());
3131
3132         writer.Finalize();
3133
3134         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3135         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3136
3137         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3138
3139         check_verify_result (
3140                 { dir },
3141                 {
3142                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3143                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID },
3144                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3145                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3146                 });
3147 }
3148
3149
3150 /** Check that ResourceID and the MXF ID being the same is spotted */
3151 BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
3152 {
3153         boost::filesystem::path const dir = "build/test/verify_incorrect_timed_text_id";
3154         prepare_directory (dir);
3155
3156         ASDCP::WriterInfo writer_info;
3157         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3158
3159         unsigned int c;
3160         auto mxf_id = dcp::make_uuid ();
3161         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3162         BOOST_REQUIRE (c == Kumu::UUID_Length);
3163
3164         auto resource_id = mxf_id;
3165         ASDCP::TimedText::TimedTextDescriptor descriptor;
3166         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3167         DCP_ASSERT (c == Kumu::UUID_Length);
3168
3169         auto xml_id = resource_id;
3170         ASDCP::TimedText::MXFWriter writer;
3171         auto subs_mxf = dir / "subs.mxf";
3172         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3173         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3174         writer.WriteTimedTextResource (dcp::String::compose(
3175                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3176                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3177                 "<Id>urn:uuid:%1</Id>"
3178                 "<ContentTitleText>Content</ContentTitleText>"
3179                 "<AnnotationText>Annotation</AnnotationText>"
3180                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3181                 "<ReelNumber>1</ReelNumber>"
3182                 "<Language>en-US</Language>"
3183                 "<EditRate>25 1</EditRate>"
3184                 "<TimeCodeRate>25</TimeCodeRate>"
3185                 "<StartTime>00:00:00:00</StartTime>"
3186                 "<SubtitleList>"
3187                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3188                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3189                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3190                 "</Subtitle>"
3191                 "</Font>"
3192                 "</SubtitleList>"
3193                 "</SubtitleReel>",
3194                 xml_id).c_str());
3195
3196         writer.Finalize();
3197
3198         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3199         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3200
3201         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3202
3203         check_verify_result (
3204                 { dir },
3205                 {
3206                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3207                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID },
3208                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3209                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3210                 });
3211 }
3212
3213
3214 /** Check a DCP with a 3D asset marked as 2D */
3215 BOOST_AUTO_TEST_CASE (verify_threed_marked_as_twod)
3216 {
3217         check_verify_result (
3218                 { private_test / "data" / "xm" },
3219                 {
3220                         {
3221                                 dcp::VerificationNote::Type::WARNING,
3222                                 dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, boost::filesystem::canonical(find_file(private_test / "data" / "xm", "j2c"))
3223                         },
3224                         {
3225                                 dcp::VerificationNote::Type::BV21_ERROR,
3226                                 dcp::VerificationNote::Code::INVALID_STANDARD
3227                         },
3228                 });
3229
3230 }
3231
3232
3233 BOOST_AUTO_TEST_CASE (verify_unexpected_things_in_main_markers)
3234 {
3235         path dir = "build/test/verify_unexpected_things_in_main_markers";
3236         prepare_directory (dir);
3237         auto dcp = make_simple (dir, 1, 24);
3238         dcp->set_annotation_text("A Test DCP");
3239         dcp->write_xml();
3240
3241         {
3242                 Editor e (find_cpl(dir));
3243                 e.insert(
3244                         "          <IntrinsicDuration>24</IntrinsicDuration>",
3245                         "<EntryPoint>0</EntryPoint><Duration>24</Duration>"
3246                         );
3247         }
3248
3249         dcp::CPL cpl (find_cpl(dir));
3250
3251         check_verify_result (
3252                 { dir },
3253                 {
3254                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3255                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_ENTRY_POINT },
3256                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_DURATION },
3257                 });
3258 }
3259
3260
3261 BOOST_AUTO_TEST_CASE(verify_invalid_content_kind)
3262 {
3263         path dir = "build/test/verify_invalid_content_kind";
3264         prepare_directory (dir);
3265         auto dcp = make_simple (dir, 1, 24);
3266         dcp->set_annotation_text("A Test DCP");
3267         dcp->write_xml();
3268
3269         {
3270                 Editor e(find_cpl(dir));
3271                 e.replace("trailer", "trip");
3272         }
3273
3274         dcp::CPL cpl (find_cpl(dir));
3275
3276         check_verify_result (
3277                 { dir },
3278                 {
3279                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3280                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_CONTENT_KIND, string("trip") }
3281                 });
3282
3283 }
3284
3285
3286 BOOST_AUTO_TEST_CASE(verify_valid_content_kind)
3287 {
3288         path dir = "build/test/verify_valid_content_kind";
3289         prepare_directory (dir);
3290         auto dcp = make_simple (dir, 1, 24);
3291         dcp->set_annotation_text("A Test DCP");
3292         dcp->write_xml();
3293
3294         {
3295                 Editor e(find_cpl(dir));
3296                 e.replace("<ContentKind>trailer</ContentKind>", "<ContentKind scope=\"http://bobs.contents/\">trip</ContentKind>");
3297         }
3298
3299         dcp::CPL cpl (find_cpl(dir));
3300
3301         check_verify_result (
3302                 { dir },
3303                 {
3304                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3305                 });
3306
3307 }
3308
3309
3310 BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_1)
3311 {
3312         path dir = "build/test/verify_invalid_main_picture_active_area_1";
3313         prepare_directory(dir);
3314         auto dcp = make_simple(dir, 1, 24);
3315         dcp->write_xml();
3316
3317         auto constexpr area = "<meta:MainPictureActiveArea>";
3318
3319         {
3320                 Editor e(find_cpl(dir));
3321                 e.delete_lines_after(area, 2);
3322                 e.insert(area, "<meta:Height>4080</meta:Height>");
3323                 e.insert(area, "<meta:Width>1997</meta:Width>");
3324         }
3325
3326         dcp::PKL pkl(find_pkl(dir));
3327         dcp::CPL cpl(find_cpl(dir));
3328
3329         check_verify_result(
3330                 { dir },
3331                 {
3332                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3333                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3334                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "width 1997 is not a multiple of 2", canonical(find_cpl(dir)) },
3335                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 4080 is bigger than the asset height 1080", canonical(find_cpl(dir)) },
3336                 });
3337 }
3338
3339
3340 BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_2)
3341 {
3342         path dir = "build/test/verify_invalid_main_picture_active_area_2";
3343         prepare_directory(dir);
3344         auto dcp = make_simple(dir, 1, 24);
3345         dcp->write_xml();
3346
3347         auto constexpr area = "<meta:MainPictureActiveArea>";
3348
3349         {
3350                 Editor e(find_cpl(dir));
3351                 e.delete_lines_after(area, 2);
3352                 e.insert(area, "<meta:Height>5125</meta:Height>");
3353                 e.insert(area, "<meta:Width>9900</meta:Width>");
3354         }
3355
3356         dcp::PKL pkl(find_pkl(dir));
3357         dcp::CPL cpl(find_cpl(dir));
3358
3359         check_verify_result(
3360                 { dir },
3361                 {
3362                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3363                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3364                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 5125 is not a multiple of 2", canonical(find_cpl(dir)) },
3365                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "width 9900 is bigger than the asset width 1998", canonical(find_cpl(dir)) },
3366                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 5125 is bigger than the asset height 1080", canonical(find_cpl(dir)) },
3367                 });
3368 }
3369
3370
3371 BOOST_AUTO_TEST_CASE(verify_duplicate_pkl_asset_ids)
3372 {
3373         RNGFixer rg;
3374
3375         path dir = "build/test/verify_duplicate_pkl_asset_ids";
3376         prepare_directory(dir);
3377         auto dcp = make_simple(dir, 1, 24);
3378         dcp->write_xml();
3379
3380         {
3381                 Editor e(find_pkl(dir));
3382                 e.replace("urn:uuid:5407b210-4441-4e97-8b16-8bdc7c12da54", "urn:uuid:6affb8ee-0020-4dff-a53c-17652f6358ab");
3383         }
3384
3385         dcp::PKL pkl(find_pkl(dir));
3386
3387         check_verify_result(
3388                 { dir },
3389                 {
3390                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl.id(), canonical(find_pkl(dir)) },
3391                 });
3392 }
3393
3394
3395 BOOST_AUTO_TEST_CASE(verify_duplicate_assetmap_asset_ids)
3396 {
3397         RNGFixer rg;
3398
3399         path dir = "build/test/verify_duplicate_assetmap_asset_ids";
3400         prepare_directory(dir);
3401         auto dcp = make_simple(dir, 1, 24);
3402         dcp->write_xml();
3403
3404         {
3405                 Editor e(find_asset_map(dir));
3406                 e.replace("urn:uuid:5407b210-4441-4e97-8b16-8bdc7c12da54", "urn:uuid:97f0f352-5b77-48ee-a558-9df37717f4fa");
3407         }
3408
3409         dcp::PKL pkl(find_pkl(dir));
3410         dcp::AssetMap asset_map(find_asset_map(dir));
3411
3412         check_verify_result(
3413                 { dir },
3414                 {
3415                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3416                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map.id(), canonical(find_asset_map(dir)) },
3417                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, string("5407b210-4441-4e97-8b16-8bdc7c12da54") },
3418                 });
3419 }
3420