Fix tests.
[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::ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE, asset->id(), boost::filesystem::canonical(asset->file().get()) },
922                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
923                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_TEXT },
924                 });
925 }
926
927
928 BOOST_AUTO_TEST_CASE (verify_external_asset)
929 {
930         path const ov_dir("build/test/verify_external_asset");
931         prepare_directory (ov_dir);
932
933         auto image = black_image ();
934         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
935         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
936         dcp_from_frame (frame, ov_dir);
937
938         dcp::DCP ov (ov_dir);
939         ov.read ();
940
941         path const vf_dir("build/test/verify_external_asset_vf");
942         prepare_directory (vf_dir);
943
944         auto picture = ov.cpls()[0]->reels()[0]->main_picture();
945         auto cpl = write_dcp_with_single_asset (vf_dir, picture);
946
947         check_verify_result (
948                 { vf_dir },
949                 {
950                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, picture->asset()->id() },
951                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
952                 });
953 }
954
955
956 BOOST_AUTO_TEST_CASE (verify_valid_cpl_metadata)
957 {
958         path const dir("build/test/verify_valid_cpl_metadata");
959         prepare_directory (dir);
960
961         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
962         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
963         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
964
965         auto reel = make_shared<dcp::Reel>();
966         reel->add (reel_asset);
967
968         reel->add (make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", 16 * 24), 0));
969         reel->add (simple_markers(16 * 24));
970
971         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
972         cpl->add (reel);
973         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
974         cpl->set_main_sound_sample_rate (48000);
975         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
976         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
977         cpl->set_version_number (1);
978
979         dcp::DCP dcp (dir);
980         dcp.add (cpl);
981         dcp.set_annotation_text("hello");
982         dcp.write_xml ();
983 }
984
985
986 path
987 find_prefix(path dir, string prefix)
988 {
989         auto iter = std::find_if(directory_iterator(dir), directory_iterator(), [prefix](path const& p) {
990                 return boost::starts_with(p.filename().string(), prefix);
991         });
992
993         BOOST_REQUIRE(iter != directory_iterator());
994         return iter->path();
995 }
996
997
998 path find_cpl (path dir)
999 {
1000         return find_prefix(dir, "cpl_");
1001 }
1002
1003
1004 path
1005 find_pkl(path dir)
1006 {
1007         return find_prefix(dir, "pkl_");
1008 }
1009
1010
1011 path
1012 find_asset_map(path dir)
1013 {
1014         return find_prefix(dir, "ASSETMAP");
1015 }
1016
1017
1018 /* DCP with invalid CompositionMetadataAsset */
1019 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_bad_tag)
1020 {
1021         using namespace boost::filesystem;
1022
1023         path const dir("build/test/verify_invalid_cpl_metadata_bad_tag");
1024         prepare_directory (dir);
1025
1026         auto reel = make_shared<dcp::Reel>();
1027         reel->add (black_picture_asset(dir));
1028         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1029         cpl->add (reel);
1030         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1031         cpl->set_main_sound_sample_rate (48000);
1032         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1033         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1034         cpl->set_version_number (1);
1035
1036         reel->add (simple_markers());
1037
1038         dcp::DCP dcp (dir);
1039         dcp.add (cpl);
1040         dcp.set_annotation_text("hello");
1041         dcp.write_xml();
1042
1043         {
1044                 Editor e (find_cpl(dir));
1045                 e.replace ("MainSound", "MainSoundX");
1046         }
1047
1048         check_verify_result (
1049                 { dir },
1050                 {
1051                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXConfiguration'"), canonical(cpl->file().get()), 50 },
1052                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXSampleRate'"), canonical(cpl->file().get()), 51 },
1053                         {
1054                                 dcp::VerificationNote::Type::ERROR,
1055                                 dcp::VerificationNote::Code::INVALID_XML,
1056                                 string("element 'meta:MainSoundXConfiguration' is not allowed for content model "
1057                                        "'(Id,AnnotationText?,EditRate,IntrinsicDuration,EntryPoint?,Duration?,"
1058                                        "FullContentTitleText,ReleaseTerritory?,VersionNumber?,Chain?,Distributor?,"
1059                                        "Facility?,AlternateContentVersionList?,Luminance?,MainSoundConfiguration,"
1060                                        "MainSoundSampleRate,MainPictureStoredArea,MainPictureActiveArea,MainSubtitleLanguageList?,"
1061                                        "ExtensionMetadataList?,)'"),
1062                                 canonical(cpl->file().get()),
1063                                 71
1064                         },
1065                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) },
1066                 });
1067 }
1068
1069
1070 /* DCP with invalid CompositionMetadataAsset */
1071 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_missing_tag)
1072 {
1073         path const dir("build/test/verify_invalid_cpl_metadata_missing_tag");
1074         prepare_directory (dir);
1075
1076         auto reel = make_shared<dcp::Reel>();
1077         reel->add (black_picture_asset(dir));
1078         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1079         cpl->add (reel);
1080         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1081         cpl->set_main_sound_sample_rate (48000);
1082         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1083         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1084
1085         dcp::DCP dcp (dir);
1086         dcp.add (cpl);
1087         dcp.set_annotation_text("hello");
1088         dcp.write_xml();
1089
1090         {
1091                 Editor e (find_cpl(dir));
1092                 e.replace ("meta:Width", "meta:WidthX");
1093         }
1094
1095         check_verify_result (
1096                 { dir },
1097                 {{ dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::FAILED_READ, string("missing XML tag Width in MainPictureStoredArea") }}
1098                 );
1099 }
1100
1101
1102 BOOST_AUTO_TEST_CASE (verify_invalid_language1)
1103 {
1104         path const dir("build/test/verify_invalid_language1");
1105         prepare_directory (dir);
1106         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
1107         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
1108         asset->_language = "wrong-andbad";
1109         asset->write (dir / "subs.mxf");
1110         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
1111         reel_asset->_language = "badlang";
1112         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1113
1114         check_verify_result (
1115                 { dir },
1116                 {
1117                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
1118                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
1119                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1120                 });
1121 }
1122
1123
1124 /* SMPTE DCP with invalid <Language> in the MainClosedCaption reel and also in the XML within the MXF */
1125 BOOST_AUTO_TEST_CASE (verify_invalid_language2)
1126 {
1127         path const dir("build/test/verify_invalid_language2");
1128         prepare_directory (dir);
1129         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
1130         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
1131         asset->_language = "wrong-andbad";
1132         asset->write (dir / "subs.mxf");
1133         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
1134         reel_asset->_language = "badlang";
1135         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1136
1137         check_verify_result (
1138                 {dir},
1139                 {
1140                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
1141                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
1142                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1143                 });
1144 }
1145
1146
1147 /* SMPTE DCP with invalid <Language> in the MainSound reel, the CPL additional subtitles languages and
1148  * the release territory.
1149  */
1150 BOOST_AUTO_TEST_CASE (verify_invalid_language3)
1151 {
1152         path const dir("build/test/verify_invalid_language3");
1153         prepare_directory (dir);
1154
1155         auto picture = simple_picture (dir, "foo");
1156         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
1157         auto reel = make_shared<dcp::Reel>();
1158         reel->add (reel_picture);
1159         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "frobozz");
1160         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
1161         reel->add (reel_sound);
1162         reel->add (simple_markers());
1163
1164         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1165         cpl->add (reel);
1166         cpl->_additional_subtitle_languages.push_back("this-is-wrong");
1167         cpl->_additional_subtitle_languages.push_back("andso-is-this");
1168         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1169         cpl->set_main_sound_sample_rate (48000);
1170         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1171         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1172         cpl->set_version_number (1);
1173         cpl->_release_territory = "fred-jim";
1174         auto dcp = make_shared<dcp::DCP>(dir);
1175         dcp->add (cpl);
1176         dcp->set_annotation_text("hello");
1177         dcp->write_xml();
1178
1179         check_verify_result (
1180                 { dir },
1181                 {
1182                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("this-is-wrong") },
1183                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("andso-is-this") },
1184                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("fred-jim") },
1185                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("frobozz") },
1186                 });
1187 }
1188
1189
1190 static
1191 vector<dcp::VerificationNote>
1192 check_picture_size (int width, int height, int frame_rate, bool three_d)
1193 {
1194         using namespace boost::filesystem;
1195
1196         path dcp_path = "build/test/verify_picture_test";
1197         prepare_directory (dcp_path);
1198
1199         shared_ptr<dcp::PictureAsset> mp;
1200         if (three_d) {
1201                 mp = make_shared<dcp::StereoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1202         } else {
1203                 mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1204         }
1205         auto picture_writer = mp->start_write (dcp_path / "video.mxf", false);
1206
1207         auto image = black_image (dcp::Size(width, height));
1208         auto j2c = dcp::compress_j2k (image, 100000000, frame_rate, three_d, width > 2048);
1209         int const length = three_d ? frame_rate * 2 : frame_rate;
1210         for (int i = 0; i < length; ++i) {
1211                 picture_writer->write (j2c.data(), j2c.size());
1212         }
1213         picture_writer->finalize ();
1214
1215         auto d = make_shared<dcp::DCP>(dcp_path);
1216         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1217         cpl->set_annotation_text ("A Test DCP");
1218         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
1219         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1220         cpl->set_main_sound_sample_rate (48000);
1221         cpl->set_main_picture_stored_area(dcp::Size(width, height));
1222         cpl->set_main_picture_active_area(dcp::Size(width, height));
1223         cpl->set_version_number (1);
1224
1225         auto reel = make_shared<dcp::Reel>();
1226
1227         if (three_d) {
1228                 reel->add (make_shared<dcp::ReelStereoPictureAsset>(std::dynamic_pointer_cast<dcp::StereoPictureAsset>(mp), 0));
1229         } else {
1230                 reel->add (make_shared<dcp::ReelMonoPictureAsset>(std::dynamic_pointer_cast<dcp::MonoPictureAsset>(mp), 0));
1231         }
1232
1233         reel->add (simple_markers(frame_rate));
1234
1235         cpl->add (reel);
1236
1237         d->add (cpl);
1238         d->set_annotation_text("A Test DCP");
1239         d->write_xml();
1240
1241         return dcp::verify ({dcp_path}, &stage, &progress, xsd_test);
1242 }
1243
1244
1245 static
1246 void
1247 check_picture_size_ok (int width, int height, int frame_rate, bool three_d)
1248 {
1249         auto notes = check_picture_size(width, height, frame_rate, three_d);
1250         BOOST_CHECK_EQUAL (notes.size(), 0U);
1251 }
1252
1253
1254 static
1255 void
1256 check_picture_size_bad_frame_size (int width, int height, int frame_rate, bool three_d)
1257 {
1258         auto notes = check_picture_size(width, height, frame_rate, three_d);
1259         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1260         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1261         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS);
1262 }
1263
1264
1265 static
1266 void
1267 check_picture_size_bad_2k_frame_rate (int width, int height, int frame_rate, bool three_d)
1268 {
1269         auto notes = check_picture_size(width, height, frame_rate, three_d);
1270         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
1271         BOOST_CHECK_EQUAL (notes.back().type(), dcp::VerificationNote::Type::BV21_ERROR);
1272         BOOST_CHECK_EQUAL (notes.back().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K);
1273 }
1274
1275
1276 static
1277 void
1278 check_picture_size_bad_4k_frame_rate (int width, int height, int frame_rate, bool three_d)
1279 {
1280         auto notes = check_picture_size(width, height, frame_rate, three_d);
1281         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1282         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1283         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K);
1284 }
1285
1286
1287 BOOST_AUTO_TEST_CASE (verify_picture_size)
1288 {
1289         using namespace boost::filesystem;
1290
1291         /* 2K scope */
1292         check_picture_size_ok (2048, 858, 24, false);
1293         check_picture_size_ok (2048, 858, 25, false);
1294         check_picture_size_ok (2048, 858, 48, false);
1295         check_picture_size_ok (2048, 858, 24, true);
1296         check_picture_size_ok (2048, 858, 25, true);
1297         check_picture_size_ok (2048, 858, 48, true);
1298
1299         /* 2K flat */
1300         check_picture_size_ok (1998, 1080, 24, false);
1301         check_picture_size_ok (1998, 1080, 25, false);
1302         check_picture_size_ok (1998, 1080, 48, false);
1303         check_picture_size_ok (1998, 1080, 24, true);
1304         check_picture_size_ok (1998, 1080, 25, true);
1305         check_picture_size_ok (1998, 1080, 48, true);
1306
1307         /* 4K scope */
1308         check_picture_size_ok (4096, 1716, 24, false);
1309
1310         /* 4K flat */
1311         check_picture_size_ok (3996, 2160, 24, false);
1312
1313         /* Bad frame size */
1314         check_picture_size_bad_frame_size (2050, 858, 24, false);
1315         check_picture_size_bad_frame_size (2048, 658, 25, false);
1316         check_picture_size_bad_frame_size (1920, 1080, 48, true);
1317         check_picture_size_bad_frame_size (4000, 2000, 24, true);
1318
1319         /* Bad 2K frame rate */
1320         check_picture_size_bad_2k_frame_rate (2048, 858, 26, false);
1321         check_picture_size_bad_2k_frame_rate (2048, 858, 31, false);
1322         check_picture_size_bad_2k_frame_rate (1998, 1080, 50, true);
1323
1324         /* Bad 4K frame rate */
1325         check_picture_size_bad_4k_frame_rate (3996, 2160, 25, false);
1326         check_picture_size_bad_4k_frame_rate (3996, 2160, 48, false);
1327
1328         /* No 4K 3D */
1329         auto notes = check_picture_size(3996, 2160, 24, true);
1330         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1331         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1332         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D);
1333 }
1334
1335
1336 static
1337 void
1338 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")
1339 {
1340         asset->add (
1341                 std::make_shared<dcp::SubtitleString>(
1342                         optional<string>(),
1343                         false,
1344                         false,
1345                         false,
1346                         dcp::Colour(),
1347                         42,
1348                         1,
1349                         dcp::Time(start_frame, 24, 24),
1350                         dcp::Time(end_frame, 24, 24),
1351                         0,
1352                         dcp::HAlign::CENTER,
1353                         v_position,
1354                         v_align,
1355                         0,
1356                         dcp::Direction::LTR,
1357                         text,
1358                         dcp::Effect::NONE,
1359                         dcp::Colour(),
1360                         dcp::Time(),
1361                         dcp::Time(),
1362                         0
1363                 )
1364         );
1365 }
1366
1367
1368 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
1369 {
1370         path const dir("build/test/verify_invalid_closed_caption_xml_size_in_bytes");
1371         prepare_directory (dir);
1372
1373         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1374         for (int i = 0; i < 2048; ++i) {
1375                 add_test_subtitle (asset, i * 24, i * 24 + 20);
1376         }
1377         asset->set_language (dcp::LanguageTag("de-DE"));
1378         asset->write (dir / "subs.mxf");
1379         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
1380         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1381
1382         check_verify_result (
1383                 { dir },
1384                 {
1385                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1386                         {
1387                                 dcp::VerificationNote::Type::BV21_ERROR,
1388                                 dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES,
1389                                 string("419346"),
1390                                 canonical(dir / "subs.mxf")
1391                         },
1392                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1393                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1394                 });
1395 }
1396
1397
1398 static
1399 shared_ptr<dcp::SMPTESubtitleAsset>
1400 make_large_subtitle_asset (path font_file)
1401 {
1402         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1403         dcp::ArrayData big_fake_font(1024 * 1024);
1404         big_fake_font.write (font_file);
1405         for (int i = 0; i < 116; ++i) {
1406                 asset->add_font (dcp::String::compose("big%1", i), big_fake_font);
1407         }
1408         return asset;
1409 }
1410
1411
1412 template <class T>
1413 void
1414 verify_timed_text_asset_too_large (string name)
1415 {
1416         auto const dir = path("build/test") / name;
1417         prepare_directory (dir);
1418         auto asset = make_large_subtitle_asset (dir / "font.ttf");
1419         add_test_subtitle (asset, 0, 240);
1420         asset->set_language (dcp::LanguageTag("de-DE"));
1421         asset->write (dir / "subs.mxf");
1422
1423         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), 240, 0);
1424         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1425
1426         check_verify_result (
1427                 { dir },
1428                 {
1429                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121695542"), canonical(dir / "subs.mxf") },
1430                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, string("121634816"), canonical(dir / "subs.mxf") },
1431                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1432                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1433                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1434                 });
1435 }
1436
1437
1438 BOOST_AUTO_TEST_CASE (verify_subtitle_asset_too_large)
1439 {
1440         verify_timed_text_asset_too_large<dcp::ReelSMPTESubtitleAsset>("verify_subtitle_asset_too_large");
1441         verify_timed_text_asset_too_large<dcp::ReelSMPTEClosedCaptionAsset>("verify_closed_caption_asset_too_large");
1442 }
1443
1444
1445 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_language)
1446 {
1447         path dir = "build/test/verify_missing_subtitle_language";
1448         prepare_directory (dir);
1449         auto dcp = make_simple (dir, 1, 106);
1450
1451         string const xml =
1452                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1453                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1454                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1455                 "<ContentTitleText>Content</ContentTitleText>"
1456                 "<AnnotationText>Annotation</AnnotationText>"
1457                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1458                 "<ReelNumber>1</ReelNumber>"
1459                 "<EditRate>24 1</EditRate>"
1460                 "<TimeCodeRate>24</TimeCodeRate>"
1461                 "<StartTime>00:00:00:00</StartTime>"
1462                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1463                 "<SubtitleList>"
1464                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1465                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1466                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1467                 "</Subtitle>"
1468                 "</Font>"
1469                 "</SubtitleList>"
1470                 "</SubtitleReel>";
1471
1472         dcp::File xml_file(dir / "subs.xml", "w");
1473         BOOST_REQUIRE (xml_file);
1474         xml_file.write(xml.c_str(), xml.size(), 1);
1475         xml_file.close();
1476         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1477         subs->write (dir / "subs.mxf");
1478
1479         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1480         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1481         dcp->set_annotation_text("A Test DCP");
1482         dcp->write_xml();
1483
1484         check_verify_result (
1485                 { dir },
1486                 {
1487                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
1488                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1489                 });
1490 }
1491
1492
1493 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_languages)
1494 {
1495         path path ("build/test/verify_mismatched_subtitle_languages");
1496         auto constexpr reel_length = 192;
1497         auto dcp = make_simple (path, 2, reel_length);
1498         auto cpl = dcp->cpls()[0];
1499
1500         {
1501                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1502                 subs->set_language (dcp::LanguageTag("de-DE"));
1503                 subs->add (simple_subtitle());
1504                 subs->write (path / "subs1.mxf");
1505                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1506                 cpl->reels()[0]->add(reel_subs);
1507         }
1508
1509         {
1510                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1511                 subs->set_language (dcp::LanguageTag("en-US"));
1512                 subs->add (simple_subtitle());
1513                 subs->write (path / "subs2.mxf");
1514                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1515                 cpl->reels()[1]->add(reel_subs);
1516         }
1517
1518         dcp->set_annotation_text("A Test DCP");
1519         dcp->write_xml();
1520
1521         check_verify_result (
1522                 { path },
1523                 {
1524                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1525                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") },
1526                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES }
1527                 });
1528 }
1529
1530
1531 BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
1532 {
1533         path path ("build/test/verify_multiple_closed_caption_languages_allowed");
1534         auto constexpr reel_length = 192;
1535         auto dcp = make_simple (path, 2, reel_length);
1536         auto cpl = dcp->cpls()[0];
1537
1538         {
1539                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1540                 ccaps->set_language (dcp::LanguageTag("de-DE"));
1541                 ccaps->add (simple_subtitle());
1542                 ccaps->write (path / "subs1.mxf");
1543                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1544                 cpl->reels()[0]->add(reel_ccaps);
1545         }
1546
1547         {
1548                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1549                 ccaps->set_language (dcp::LanguageTag("en-US"));
1550                 ccaps->add (simple_subtitle());
1551                 ccaps->write (path / "subs2.mxf");
1552                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1553                 cpl->reels()[1]->add(reel_ccaps);
1554         }
1555
1556         dcp->set_annotation_text("A Test DCP");
1557         dcp->write_xml();
1558
1559         check_verify_result (
1560                 { path },
1561                 {
1562                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1563                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") }
1564                 });
1565 }
1566
1567
1568 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_start_time)
1569 {
1570         path dir = "build/test/verify_missing_subtitle_start_time";
1571         prepare_directory (dir);
1572         auto dcp = make_simple (dir, 1, 106);
1573
1574         string const xml =
1575                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1576                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1577                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1578                 "<ContentTitleText>Content</ContentTitleText>"
1579                 "<AnnotationText>Annotation</AnnotationText>"
1580                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1581                 "<ReelNumber>1</ReelNumber>"
1582                 "<Language>de-DE</Language>"
1583                 "<EditRate>24 1</EditRate>"
1584                 "<TimeCodeRate>24</TimeCodeRate>"
1585                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1586                 "<SubtitleList>"
1587                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1588                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1589                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1590                 "</Subtitle>"
1591                 "</Font>"
1592                 "</SubtitleList>"
1593                 "</SubtitleReel>";
1594
1595         dcp::File xml_file(dir / "subs.xml", "w");
1596         BOOST_REQUIRE (xml_file);
1597         xml_file.write(xml.c_str(), xml.size(), 1);
1598         xml_file.close();
1599         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1600         subs->write (dir / "subs.mxf");
1601
1602         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1603         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1604         dcp->set_annotation_text("A Test DCP");
1605         dcp->write_xml();
1606
1607         check_verify_result (
1608                 { dir },
1609                 {
1610                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1611                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1612                 });
1613 }
1614
1615
1616 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_start_time)
1617 {
1618         path dir = "build/test/verify_invalid_subtitle_start_time";
1619         prepare_directory (dir);
1620         auto dcp = make_simple (dir, 1, 106);
1621
1622         string const xml =
1623                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1624                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1625                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1626                 "<ContentTitleText>Content</ContentTitleText>"
1627                 "<AnnotationText>Annotation</AnnotationText>"
1628                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1629                 "<ReelNumber>1</ReelNumber>"
1630                 "<Language>de-DE</Language>"
1631                 "<EditRate>24 1</EditRate>"
1632                 "<TimeCodeRate>24</TimeCodeRate>"
1633                 "<StartTime>00:00:02:00</StartTime>"
1634                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1635                 "<SubtitleList>"
1636                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1637                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1638                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1639                 "</Subtitle>"
1640                 "</Font>"
1641                 "</SubtitleList>"
1642                 "</SubtitleReel>";
1643
1644         dcp::File xml_file(dir / "subs.xml", "w");
1645         BOOST_REQUIRE (xml_file);
1646         xml_file.write(xml.c_str(), xml.size(), 1);
1647         xml_file.close();
1648         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1649         subs->write (dir / "subs.mxf");
1650
1651         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1652         dcp->cpls().front()->reels().front()->add(reel_subs);
1653         dcp->set_annotation_text("A Test DCP");
1654         dcp->write_xml();
1655
1656         check_verify_result (
1657                 { dir },
1658                 {
1659                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1660                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1661                 });
1662 }
1663
1664
1665 class TestText
1666 {
1667 public:
1668         TestText (int in_, int out_, float v_position_ = 0, dcp::VAlign v_align_ = dcp::VAlign::CENTER, string text_ = "Hello")
1669                 : in(in_)
1670                 , out(out_)
1671                 , v_position(v_position_)
1672                 , v_align(v_align_)
1673                 , text(text_)
1674         {}
1675
1676         int in;
1677         int out;
1678         float v_position;
1679         dcp::VAlign v_align;
1680         string text;
1681 };
1682
1683
1684 template <class T>
1685 shared_ptr<dcp::CPL>
1686 dcp_with_text (path dir, vector<TestText> subs)
1687 {
1688         prepare_directory (dir);
1689         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1690         asset->set_start_time (dcp::Time());
1691         for (auto i: subs) {
1692                 add_test_subtitle (asset, i.in, i.out, i.v_position, i.v_align, i.text);
1693         }
1694         asset->set_language (dcp::LanguageTag("de-DE"));
1695         asset->write (dir / "subs.mxf");
1696
1697         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1698         return write_dcp_with_single_asset (dir, reel_asset);
1699 }
1700
1701
1702 template <class T>
1703 shared_ptr<dcp::CPL>
1704 dcp_with_text_from_file (path dir, boost::filesystem::path subs_xml)
1705 {
1706         prepare_directory (dir);
1707         auto asset = make_shared<dcp::SMPTESubtitleAsset>(subs_xml);
1708         asset->set_start_time (dcp::Time());
1709         asset->set_language (dcp::LanguageTag("de-DE"));
1710
1711         auto subs_mxf = dir / "subs.mxf";
1712         asset->write (subs_mxf);
1713
1714         /* The call to write() puts the asset into the DCP correctly but it will have
1715          * XML re-written by our parser.  Overwrite the MXF using the given file's verbatim
1716          * contents.
1717          */
1718         ASDCP::TimedText::MXFWriter writer;
1719         ASDCP::WriterInfo writer_info;
1720         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
1721         unsigned int c;
1722         Kumu::hex2bin (asset->id().c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
1723         DCP_ASSERT (c == Kumu::UUID_Length);
1724         ASDCP::TimedText::TimedTextDescriptor descriptor;
1725         descriptor.ContainerDuration = asset->intrinsic_duration();
1726         Kumu::hex2bin (asset->xml_id()->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
1727         DCP_ASSERT (c == Kumu::UUID_Length);
1728         ASDCP::Result_t r = writer.OpenWrite (subs_mxf.string().c_str(), writer_info, descriptor, 16384);
1729         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1730         r = writer.WriteTimedTextResource (dcp::file_to_string(subs_xml));
1731         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1732         writer.Finalize ();
1733
1734         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1735         return write_dcp_with_single_asset (dir, reel_asset);
1736 }
1737
1738
1739 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
1740 {
1741         auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
1742         /* Just too early */
1743         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
1744         check_verify_result (
1745                 { dir },
1746                 {
1747                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1748                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1749                 });
1750
1751 }
1752
1753
1754 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time)
1755 {
1756         auto const dir = path("build/test/verify_valid_subtitle_first_text_time");
1757         /* Just late enough */
1758         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
1759         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1760 }
1761
1762
1763 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time_on_second_reel)
1764 {
1765         auto const dir = path("build/test/verify_valid_subtitle_first_text_time_on_second_reel");
1766         prepare_directory (dir);
1767
1768         auto asset1 = make_shared<dcp::SMPTESubtitleAsset>();
1769         asset1->set_start_time (dcp::Time());
1770         /* Just late enough */
1771         add_test_subtitle (asset1, 4 * 24, 5 * 24);
1772         asset1->set_language (dcp::LanguageTag("de-DE"));
1773         asset1->write (dir / "subs1.mxf");
1774         auto reel_asset1 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset1, dcp::Fraction(24, 1), 5 * 24, 0);
1775         auto reel1 = make_shared<dcp::Reel>();
1776         reel1->add (reel_asset1);
1777         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 5 * 24);
1778         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
1779         reel1->add (markers1);
1780
1781         auto asset2 = make_shared<dcp::SMPTESubtitleAsset>();
1782         asset2->set_start_time (dcp::Time());
1783         /* This would be too early on first reel but should be OK on the second */
1784         add_test_subtitle (asset2, 3, 4 * 24);
1785         asset2->set_language (dcp::LanguageTag("de-DE"));
1786         asset2->write (dir / "subs2.mxf");
1787         auto reel_asset2 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset2, dcp::Fraction(24, 1), 4 * 24, 0);
1788         auto reel2 = make_shared<dcp::Reel>();
1789         reel2->add (reel_asset2);
1790         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 4 * 24);
1791         markers2->set (dcp::Marker::LFOC, dcp::Time(4 * 24 - 1, 24, 24));
1792         reel2->add (markers2);
1793
1794         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1795         cpl->add (reel1);
1796         cpl->add (reel2);
1797         auto dcp = make_shared<dcp::DCP>(dir);
1798         dcp->add (cpl);
1799         dcp->set_annotation_text("hello");
1800         dcp->write_xml();
1801
1802         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1803 }
1804
1805
1806 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_spacing)
1807 {
1808         auto const dir = path("build/test/verify_invalid_subtitle_spacing");
1809         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1810                 dir,
1811                 {
1812                         { 4 * 24,     5 * 24 },
1813                         { 5 * 24 + 1, 6 * 24 },
1814                 });
1815         check_verify_result (
1816                 {dir},
1817                 {
1818                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING },
1819                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1820                 });
1821 }
1822
1823
1824 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_spacing)
1825 {
1826         auto const dir = path("build/test/verify_valid_subtitle_spacing");
1827         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1828                 dir,
1829                 {
1830                         { 4 * 24,      5 * 24 },
1831                         { 5 * 24 + 16, 8 * 24 },
1832                 });
1833         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1834 }
1835
1836
1837 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_duration)
1838 {
1839         auto const dir = path("build/test/verify_invalid_subtitle_duration");
1840         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
1841         check_verify_result (
1842                 {dir},
1843                 {
1844                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION },
1845                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1846                 });
1847 }
1848
1849
1850 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_duration)
1851 {
1852         auto const dir = path("build/test/verify_valid_subtitle_duration");
1853         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
1854         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1855 }
1856
1857
1858 BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
1859 {
1860         auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
1861         prepare_directory (dir);
1862         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1863         asset->set_start_time (dcp::Time());
1864         add_test_subtitle (asset, 0, 4 * 24);
1865         asset->set_language (dcp::LanguageTag("de-DE"));
1866         asset->write (dir / "subs.mxf");
1867
1868         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
1869         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1870         check_verify_result (
1871                 {dir},
1872                 {
1873                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "72 96", boost::filesystem::canonical(asset->file().get()) },
1874                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1875                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
1876                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1877                 });
1878
1879 }
1880
1881
1882 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
1883 {
1884         auto const dir = path ("build/test/invalid_subtitle_line_count1");
1885         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1886                 dir,
1887                 {
1888                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1889                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1890                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1891                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1892                 });
1893         check_verify_result (
1894                 {dir},
1895                 {
1896                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1897                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1898                 });
1899 }
1900
1901
1902 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
1903 {
1904         auto const dir = path ("build/test/verify_valid_subtitle_line_count1");
1905         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1906                 dir,
1907                 {
1908                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1909                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1910                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1911                 });
1912         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1913 }
1914
1915
1916 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
1917 {
1918         auto const dir = path ("build/test/verify_invalid_subtitle_line_count2");
1919         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1920                 dir,
1921                 {
1922                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1923                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1924                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1925                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
1926                 });
1927         check_verify_result (
1928                 {dir},
1929                 {
1930                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1931                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1932                 });
1933 }
1934
1935
1936 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
1937 {
1938         auto const dir = path ("build/test/verify_valid_subtitle_line_count2");
1939         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1940                 dir,
1941                 {
1942                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1943                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1944                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1945                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
1946                 });
1947         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1948 }
1949
1950
1951 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
1952 {
1953         auto const dir = path ("build/test/verify_invalid_subtitle_line_length1");
1954         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1955                 dir,
1956                 {
1957                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123" }
1958                 });
1959         check_verify_result (
1960                 {dir},
1961                 {
1962                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH },
1963                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1964                 });
1965 }
1966
1967
1968 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
1969 {
1970         auto const dir = path ("build/test/verify_invalid_subtitle_line_length2");
1971         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1972                 dir,
1973                 {
1974                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
1975                 });
1976         check_verify_result (
1977                 {dir},
1978                 {
1979                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH },
1980                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1981                 });
1982 }
1983
1984
1985 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
1986 {
1987         auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
1988         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1989                 dir,
1990                 {
1991                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1992                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1993                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1994                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1995                 });
1996         check_verify_result (
1997                 {dir},
1998                 {
1999                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
2000                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2001                 });
2002 }
2003
2004
2005 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
2006 {
2007         auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
2008         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2009                 dir,
2010                 {
2011                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
2012                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
2013                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
2014                 });
2015         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2016 }
2017
2018
2019 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
2020 {
2021         auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
2022         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2023                 dir,
2024                 {
2025                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
2026                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
2027                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
2028                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
2029                 });
2030         check_verify_result (
2031                 {dir},
2032                 {
2033                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
2034                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2035                 });
2036 }
2037
2038
2039 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
2040 {
2041         auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
2042         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2043                 dir,
2044                 {
2045                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
2046                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
2047                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
2048                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
2049                 });
2050         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2051 }
2052
2053
2054 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_length)
2055 {
2056         auto const dir = path ("build/test/verify_valid_closed_caption_line_length");
2057         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2058                 dir,
2059                 {
2060                         { 96, 300, 0.0, dcp::VAlign::CENTER, "01234567890123456789012345678901" }
2061                 });
2062         check_verify_result (
2063                 {dir},
2064                 {
2065                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2066                 });
2067 }
2068
2069
2070 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
2071 {
2072         auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
2073         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2074                 dir,
2075                 {
2076                         { 96, 300, 0.0, dcp::VAlign::CENTER, "0123456789012345678901234567890123" }
2077                 });
2078         check_verify_result (
2079                 {dir},
2080                 {
2081                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH },
2082                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2083                 });
2084 }
2085
2086
2087 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign1)
2088 {
2089         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign1");
2090         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2091                 dir,
2092                 {
2093                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2094                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2095                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2096                 });
2097         check_verify_result (
2098                 {dir},
2099                 {
2100                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2101                 });
2102 }
2103
2104
2105 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign2)
2106 {
2107         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign2");
2108         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2109                 dir,
2110                 {
2111                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2112                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2113                         { 96, 300, 0.2, dcp::VAlign::CENTER, "not fine" },
2114                 });
2115         check_verify_result (
2116                 {dir},
2117                 {
2118                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN },
2119                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2120                 });
2121 }
2122
2123
2124 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering1)
2125 {
2126         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering1");
2127         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2128                 dir,
2129                 {
2130                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2131                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2132                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2133                 });
2134         check_verify_result (
2135                 {dir},
2136                 {
2137                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2138                 });
2139 }
2140
2141
2142 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering2)
2143 {
2144         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering2");
2145         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2146                 dir,
2147                 {
2148                         { 96, 300, 0.2, dcp::VAlign::BOTTOM, "This" },
2149                         { 96, 300, 0.1, dcp::VAlign::BOTTOM, "is" },
2150                         { 96, 300, 0.0, dcp::VAlign::BOTTOM, "also fine" },
2151                 });
2152         check_verify_result (
2153                 {dir},
2154                 {
2155                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2156                 });
2157 }
2158
2159
2160 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering3)
2161 {
2162         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering3");
2163         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering3.xml");
2164         check_verify_result (
2165                 {dir},
2166                 {
2167                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING },
2168                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2169                 });
2170 }
2171
2172
2173 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering4)
2174 {
2175         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering4");
2176         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering4.xml");
2177         check_verify_result (
2178                 {dir},
2179                 {
2180                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2181                 });
2182 }
2183
2184
2185
2186 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
2187 {
2188         path const dir("build/test/verify_invalid_sound_frame_rate");
2189         prepare_directory (dir);
2190
2191         auto picture = simple_picture (dir, "foo");
2192         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
2193         auto reel = make_shared<dcp::Reel>();
2194         reel->add (reel_picture);
2195         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000, boost::none);
2196         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
2197         reel->add (reel_sound);
2198         reel->add (simple_markers());
2199         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2200         cpl->add (reel);
2201         auto dcp = make_shared<dcp::DCP>(dir);
2202         dcp->add (cpl);
2203         dcp->set_annotation_text("hello");
2204         dcp->write_xml();
2205
2206         check_verify_result (
2207                 {dir},
2208                 {
2209                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE, string("96000"), canonical(dir / "audiofoo.mxf") },
2210                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2211                 });
2212 }
2213
2214
2215 BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
2216 {
2217         path const dir("build/test/verify_missing_cpl_annotation_text");
2218         auto dcp = make_simple (dir);
2219         dcp->set_annotation_text("A Test DCP");
2220         dcp->write_xml();
2221
2222         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2223
2224         auto const cpl = dcp->cpls()[0];
2225
2226         {
2227                 BOOST_REQUIRE (cpl->file());
2228                 Editor e(cpl->file().get());
2229                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "");
2230         }
2231
2232         check_verify_result (
2233                 {dir},
2234                 {
2235                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2236                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2237                 });
2238 }
2239
2240
2241 BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
2242 {
2243         path const dir("build/test/verify_mismatched_cpl_annotation_text");
2244         auto dcp = make_simple (dir);
2245         dcp->set_annotation_text("A Test DCP");
2246         dcp->write_xml();
2247
2248         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2249         auto const cpl = dcp->cpls()[0];
2250
2251         {
2252                 BOOST_REQUIRE (cpl->file());
2253                 Editor e(cpl->file().get());
2254                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "<AnnotationText>A Test DCP 1</AnnotationText>");
2255         }
2256
2257         check_verify_result (
2258                 {dir},
2259                 {
2260                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2261                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2262                 });
2263 }
2264
2265
2266 BOOST_AUTO_TEST_CASE (verify_mismatched_asset_duration)
2267 {
2268         path const dir("build/test/verify_mismatched_asset_duration");
2269         prepare_directory (dir);
2270         shared_ptr<dcp::DCP> dcp (new dcp::DCP(dir));
2271         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2272
2273         shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (dir, "", 24);
2274         shared_ptr<dcp::SoundAsset> ms = simple_sound (dir, "", dcp::MXFMetadata(), "en-US", 25);
2275
2276         auto reel = make_shared<dcp::Reel>(
2277                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2278                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2279                 );
2280
2281         reel->add (simple_markers());
2282         cpl->add (reel);
2283
2284         dcp->add (cpl);
2285         dcp->set_annotation_text("A Test DCP");
2286         dcp->write_xml();
2287
2288         check_verify_result (
2289                 {dir},
2290                 {
2291                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION },
2292                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), canonical(cpl->file().get()) }
2293                 });
2294 }
2295
2296
2297
2298 static
2299 shared_ptr<dcp::CPL>
2300 verify_subtitles_must_be_in_all_reels_check (path dir, bool add_to_reel1, bool add_to_reel2)
2301 {
2302         prepare_directory (dir);
2303         auto dcp = make_shared<dcp::DCP>(dir);
2304         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2305
2306         auto constexpr reel_length = 192;
2307
2308         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2309         subs->set_language (dcp::LanguageTag("de-DE"));
2310         subs->set_start_time (dcp::Time());
2311         subs->add (simple_subtitle());
2312         subs->write (dir / "subs.mxf");
2313         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
2314
2315         auto reel1 = make_shared<dcp::Reel>(
2316                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2317                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2318                 );
2319
2320         if (add_to_reel1) {
2321                 reel1->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2322         }
2323
2324         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2325         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2326         reel1->add (markers1);
2327
2328         cpl->add (reel1);
2329
2330         auto reel2 = make_shared<dcp::Reel>(
2331                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2332                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2333                 );
2334
2335         if (add_to_reel2) {
2336                 reel2->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2337         }
2338
2339         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2340         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2341         reel2->add (markers2);
2342
2343         cpl->add (reel2);
2344
2345         dcp->add (cpl);
2346         dcp->set_annotation_text("A Test DCP");
2347         dcp->write_xml();
2348
2349         return cpl;
2350 }
2351
2352
2353 BOOST_AUTO_TEST_CASE (verify_missing_main_subtitle_from_some_reels)
2354 {
2355         {
2356                 path dir ("build/test/missing_main_subtitle_from_some_reels");
2357                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, false);
2358                 check_verify_result (
2359                         { dir },
2360                         {
2361                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS },
2362                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2363                         });
2364
2365         }
2366
2367         {
2368                 path dir ("build/test/verify_subtitles_must_be_in_all_reels2");
2369                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, true);
2370                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2371         }
2372
2373         {
2374                 path dir ("build/test/verify_subtitles_must_be_in_all_reels1");
2375                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, false, false);
2376                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2377         }
2378 }
2379
2380
2381 static
2382 shared_ptr<dcp::CPL>
2383 verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1, int caps_in_reel2)
2384 {
2385         prepare_directory (dir);
2386         auto dcp = make_shared<dcp::DCP>(dir);
2387         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2388
2389         auto constexpr reel_length = 192;
2390
2391         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2392         subs->set_language (dcp::LanguageTag("de-DE"));
2393         subs->set_start_time (dcp::Time());
2394         subs->add (simple_subtitle());
2395         subs->write (dir / "subs.mxf");
2396
2397         auto reel1 = make_shared<dcp::Reel>(
2398                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2399                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2400                 );
2401
2402         for (int i = 0; i < caps_in_reel1; ++i) {
2403                 reel1->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2404         }
2405
2406         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2407         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2408         reel1->add (markers1);
2409
2410         cpl->add (reel1);
2411
2412         auto reel2 = make_shared<dcp::Reel>(
2413                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2414                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2415                 );
2416
2417         for (int i = 0; i < caps_in_reel2; ++i) {
2418                 reel2->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2419         }
2420
2421         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2422         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2423         reel2->add (markers2);
2424
2425         cpl->add (reel2);
2426
2427         dcp->add (cpl);
2428         dcp->set_annotation_text("A Test DCP");
2429         dcp->write_xml();
2430
2431         return cpl;
2432 }
2433
2434
2435 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_asset_counts)
2436 {
2437         {
2438                 path dir ("build/test/mismatched_closed_caption_asset_counts");
2439                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 3, 4);
2440                 check_verify_result (
2441                         {dir},
2442                         {
2443                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS },
2444                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2445                         });
2446         }
2447
2448         {
2449                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels2");
2450                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 4, 4);
2451                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2452         }
2453
2454         {
2455                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels3");
2456                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 0, 0);
2457                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2458         }
2459 }
2460
2461
2462 template <class T>
2463 void
2464 verify_text_entry_point_check (path dir, dcp::VerificationNote::Code code, boost::function<void (shared_ptr<T>)> adjust)
2465 {
2466         prepare_directory (dir);
2467         auto dcp = make_shared<dcp::DCP>(dir);
2468         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2469
2470         auto constexpr reel_length = 192;
2471
2472         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2473         subs->set_language (dcp::LanguageTag("de-DE"));
2474         subs->set_start_time (dcp::Time());
2475         subs->add (simple_subtitle());
2476         subs->write (dir / "subs.mxf");
2477         auto reel_text = make_shared<T>(subs, dcp::Fraction(24, 1), reel_length, 0);
2478         adjust (reel_text);
2479
2480         auto reel = make_shared<dcp::Reel>(
2481                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2482                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2483                 );
2484
2485         reel->add (reel_text);
2486
2487         reel->add (simple_markers(reel_length));
2488
2489         cpl->add (reel);
2490
2491         dcp->add (cpl);
2492         dcp->set_annotation_text("A Test DCP");
2493         dcp->write_xml();
2494
2495         check_verify_result (
2496                 {dir},
2497                 {
2498                         { dcp::VerificationNote::Type::BV21_ERROR, code, subs->id() },
2499                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2500                 });
2501 }
2502
2503
2504 BOOST_AUTO_TEST_CASE (verify_text_entry_point)
2505 {
2506         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2507                 "build/test/verify_subtitle_entry_point_must_be_present",
2508                 dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT,
2509                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2510                         asset->unset_entry_point ();
2511                         }
2512                 );
2513
2514         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2515                 "build/test/verify_subtitle_entry_point_must_be_zero",
2516                 dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT,
2517                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2518                         asset->set_entry_point (4);
2519                         }
2520                 );
2521
2522         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2523                 "build/test/verify_closed_caption_entry_point_must_be_present",
2524                 dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
2525                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2526                         asset->unset_entry_point ();
2527                         }
2528                 );
2529
2530         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2531                 "build/test/verify_closed_caption_entry_point_must_be_zero",
2532                 dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
2533                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2534                         asset->set_entry_point (9);
2535                         }
2536                 );
2537 }
2538
2539
2540 BOOST_AUTO_TEST_CASE (verify_missing_hash)
2541 {
2542         RNGFixer fix;
2543
2544         path const dir("build/test/verify_missing_hash");
2545         auto dcp = make_simple (dir);
2546         dcp->set_annotation_text("A Test DCP");
2547         dcp->write_xml();
2548
2549         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2550         auto const cpl = dcp->cpls()[0];
2551         BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
2552         BOOST_REQUIRE (cpl->reels()[0]->main_picture());
2553         auto asset_id = cpl->reels()[0]->main_picture()->id();
2554
2555         {
2556                 BOOST_REQUIRE (cpl->file());
2557                 Editor e(cpl->file().get());
2558                 e.delete_first_line_containing("<Hash>");
2559         }
2560
2561         check_verify_result (
2562                 {dir},
2563                 {
2564                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2565                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
2566                 });
2567 }
2568
2569
2570 static
2571 void
2572 verify_markers_test (
2573         path dir,
2574         vector<pair<dcp::Marker, dcp::Time>> markers,
2575         vector<dcp::VerificationNote> test_notes
2576         )
2577 {
2578         auto dcp = make_simple (dir);
2579         dcp->cpls()[0]->set_content_kind (dcp::ContentKind::FEATURE);
2580         auto markers_asset = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 24);
2581         for (auto const& i: markers) {
2582                 markers_asset->set (i.first, i.second);
2583         }
2584         dcp->cpls()[0]->reels()[0]->add(markers_asset);
2585         dcp->set_annotation_text("A Test DCP");
2586         dcp->write_xml();
2587
2588         check_verify_result ({dir}, test_notes);
2589 }
2590
2591
2592 BOOST_AUTO_TEST_CASE (verify_markers)
2593 {
2594         verify_markers_test (
2595                 "build/test/verify_markers_all_correct",
2596                 {
2597                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2598                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2599                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2600                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2601                 },
2602                 {}
2603                 );
2604
2605         verify_markers_test (
2606                 "build/test/verify_markers_missing_ffec",
2607                 {
2608                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2609                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2610                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2611                 },
2612                 {
2613                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE }
2614                 });
2615
2616         verify_markers_test (
2617                 "build/test/verify_markers_missing_ffmc",
2618                 {
2619                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2620                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2621                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2622                 },
2623                 {
2624                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
2625                 });
2626
2627         verify_markers_test (
2628                 "build/test/verify_markers_missing_ffoc",
2629                 {
2630                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2631                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2632                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2633                 },
2634                 {
2635                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC}
2636                 });
2637
2638         verify_markers_test (
2639                 "build/test/verify_markers_missing_lfoc",
2640                 {
2641                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2642                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2643                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) }
2644                 },
2645                 {
2646                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC }
2647                 });
2648
2649         verify_markers_test (
2650                 "build/test/verify_markers_incorrect_ffoc",
2651                 {
2652                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2653                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2654                         { dcp::Marker::FFOC, dcp::Time(3, 24, 24) },
2655                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2656                 },
2657                 {
2658                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_FFOC, string("3") }
2659                 });
2660
2661         verify_markers_test (
2662                 "build/test/verify_markers_incorrect_lfoc",
2663                 {
2664                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2665                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2666                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2667                         { dcp::Marker::LFOC, dcp::Time(18, 24, 24) }
2668                 },
2669                 {
2670                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_LFOC, string("18") }
2671                 });
2672 }
2673
2674
2675 BOOST_AUTO_TEST_CASE (verify_missing_cpl_metadata_version_number)
2676 {
2677         path dir = "build/test/verify_missing_cpl_metadata_version_number";
2678         prepare_directory (dir);
2679         auto dcp = make_simple (dir);
2680         auto cpl = dcp->cpls()[0];
2681         cpl->unset_version_number();
2682         dcp->set_annotation_text("A Test DCP");
2683         dcp->write_xml();
2684
2685         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get() }});
2686 }
2687
2688
2689 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
2690 {
2691         path dir = "build/test/verify_missing_extension_metadata1";
2692         auto dcp = make_simple (dir);
2693         dcp->set_annotation_text("A Test DCP");
2694         dcp->write_xml();
2695
2696         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2697         auto cpl = dcp->cpls()[0];
2698
2699         {
2700                 Editor e (cpl->file().get());
2701                 e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
2702         }
2703
2704         check_verify_result (
2705                 {dir},
2706                 {
2707                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2708                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2709                 });
2710 }
2711
2712
2713 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
2714 {
2715         path dir = "build/test/verify_missing_extension_metadata2";
2716         auto dcp = make_simple (dir);
2717         dcp->set_annotation_text("A Test DCP");
2718         dcp->write_xml();
2719
2720         auto cpl = dcp->cpls()[0];
2721
2722         {
2723                 Editor e (cpl->file().get());
2724                 e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
2725         }
2726
2727         check_verify_result (
2728                 {dir},
2729                 {
2730                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2731                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2732                 });
2733 }
2734
2735
2736 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
2737 {
2738         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata3";
2739         auto dcp = make_simple (dir);
2740         dcp->set_annotation_text("A Test DCP");
2741         dcp->write_xml();
2742
2743         auto const cpl = dcp->cpls()[0];
2744
2745         {
2746                 Editor e (cpl->file().get());
2747                 e.replace ("<meta:Name>A", "<meta:NameX>A");
2748                 e.replace ("n</meta:Name>", "n</meta:NameX>");
2749         }
2750
2751         check_verify_result (
2752                 {dir},
2753                 {
2754                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:NameX'"), cpl->file().get(), 70 },
2755                         { 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 },
2756                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2757                 });
2758 }
2759
2760
2761 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
2762 {
2763         path dir = "build/test/verify_invalid_extension_metadata1";
2764         auto dcp = make_simple (dir);
2765         dcp->set_annotation_text("A Test DCP");
2766         dcp->write_xml();
2767
2768         auto cpl = dcp->cpls()[0];
2769
2770         {
2771                 Editor e (cpl->file().get());
2772                 e.replace ("Application", "Fred");
2773         }
2774
2775         check_verify_result (
2776                 {dir},
2777                 {
2778                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2779                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
2780                 });
2781 }
2782
2783
2784 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
2785 {
2786         path dir = "build/test/verify_invalid_extension_metadata2";
2787         auto dcp = make_simple (dir);
2788         dcp->set_annotation_text("A Test DCP");
2789         dcp->write_xml();
2790
2791         auto cpl = dcp->cpls()[0];
2792
2793         {
2794                 Editor e (cpl->file().get());
2795                 e.replace ("DCP Constraints Profile", "Fred");
2796         }
2797
2798         check_verify_result (
2799                 {dir},
2800                 {
2801                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2802                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
2803                 });
2804 }
2805
2806
2807 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
2808 {
2809         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata6";
2810         auto dcp = make_simple (dir);
2811         dcp->set_annotation_text("A Test DCP");
2812         dcp->write_xml();
2813
2814         auto const cpl = dcp->cpls()[0];
2815
2816         {
2817                 Editor e (cpl->file().get());
2818                 e.replace ("<meta:Value>", "<meta:ValueX>");
2819                 e.replace ("</meta:Value>", "</meta:ValueX>");
2820         }
2821
2822         check_verify_result (
2823                 {dir},
2824                 {
2825                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 74 },
2826                         { 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 },
2827                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2828                 });
2829 }
2830
2831
2832 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
2833 {
2834         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata7";
2835         auto dcp = make_simple (dir);
2836         dcp->set_annotation_text("A Test DCP");
2837         dcp->write_xml();
2838
2839         auto const cpl = dcp->cpls()[0];
2840
2841         {
2842                 Editor e (cpl->file().get());
2843                 e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
2844         }
2845
2846         check_verify_result (
2847                 {dir},
2848                 {
2849                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2850                         { 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() },
2851                 });
2852 }
2853
2854
2855 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
2856 {
2857         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata8";
2858         auto dcp = make_simple (dir);
2859         dcp->set_annotation_text("A Test DCP");
2860         dcp->write_xml();
2861
2862         auto const cpl = dcp->cpls()[0];
2863
2864         {
2865                 Editor e (cpl->file().get());
2866                 e.replace ("<meta:Property>", "<meta:PropertyX>");
2867                 e.replace ("</meta:Property>", "</meta:PropertyX>");
2868         }
2869
2870         check_verify_result (
2871                 {dir},
2872                 {
2873                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 72 },
2874                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 76 },
2875                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2876                 });
2877 }
2878
2879
2880 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
2881 {
2882         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata9";
2883         auto dcp = make_simple (dir);
2884         dcp->set_annotation_text("A Test DCP");
2885         dcp->write_xml();
2886
2887         auto const cpl = dcp->cpls()[0];
2888
2889         {
2890                 Editor e (cpl->file().get());
2891                 e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
2892                 e.replace ("</meta:PropertyList>", "</meta:PropertyListX>");
2893         }
2894
2895         check_verify_result (
2896                 {dir},
2897                 {
2898                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 71 },
2899                         { 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 },
2900                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2901                 });
2902 }
2903
2904
2905
2906 BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
2907 {
2908         path dir = "build/test/verify_unsigned_cpl_with_encrypted_content";
2909         prepare_directory (dir);
2910         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2911                 copy_file (i.path(), dir / i.path().filename());
2912         }
2913
2914         path const pkl = dir / ( "pkl_" + encryption_test_pkl_id + ".xml" );
2915         path const cpl = dir / ( "cpl_" + encryption_test_cpl_id + ".xml");
2916
2917         {
2918                 Editor e (cpl);
2919                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2920         }
2921
2922         check_verify_result (
2923                 {dir},
2924                 {
2925                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id, canonical(cpl) },
2926                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl), },
2927                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2928                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2929                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2930                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2931                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2932                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, encryption_test_cpl_id, canonical(cpl) }
2933                 });
2934 }
2935
2936
2937 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_encrypted_content)
2938 {
2939         path dir = "build/test/unsigned_pkl_with_encrypted_content";
2940         prepare_directory (dir);
2941         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2942                 copy_file (i.path(), dir / i.path().filename());
2943         }
2944
2945         path const cpl = dir / ("cpl_" + encryption_test_cpl_id + ".xml");
2946         path const pkl = dir / ("pkl_" + encryption_test_pkl_id + ".xml");
2947         {
2948                 Editor e (pkl);
2949                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2950         }
2951
2952         check_verify_result (
2953                 {dir},
2954                 {
2955                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl) },
2956                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2957                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2958                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2959                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2960                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2961                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, encryption_test_pkl_id, canonical(pkl) },
2962                 });
2963 }
2964
2965
2966 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_unencrypted_content)
2967 {
2968         path dir = "build/test/verify_unsigned_pkl_with_unencrypted_content";
2969         prepare_directory (dir);
2970         for (auto i: directory_iterator("test/ref/DCP/dcp_test1")) {
2971                 copy_file (i.path(), dir / i.path().filename());
2972         }
2973
2974         {
2975                 Editor e (dir / dcp_test1_pkl);
2976                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2977         }
2978
2979         check_verify_result ({dir}, {});
2980 }
2981
2982
2983 BOOST_AUTO_TEST_CASE (verify_partially_encrypted)
2984 {
2985         path dir ("build/test/verify_must_not_be_partially_encrypted");
2986         prepare_directory (dir);
2987
2988         dcp::DCP d (dir);
2989
2990         auto signer = make_shared<dcp::CertificateChain>();
2991         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem")));
2992         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem")));
2993         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem")));
2994         signer->set_key (dcp::file_to_string("test/ref/crypt/leaf.key"));
2995
2996         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2997
2998         dcp::Key key;
2999
3000         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
3001         mp->set_key (key);
3002
3003         auto writer = mp->start_write (dir / "video.mxf", false);
3004         dcp::ArrayData j2c ("test/data/flat_red.j2c");
3005         for (int i = 0; i < 24; ++i) {
3006                 writer->write (j2c.data(), j2c.size());
3007         }
3008         writer->finalize ();
3009
3010         auto ms = simple_sound (dir, "", dcp::MXFMetadata(), "de-DE");
3011
3012         auto reel = make_shared<dcp::Reel>(
3013                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3014                 make_shared<dcp::ReelSoundAsset>(ms, 0)
3015                 );
3016
3017         reel->add (simple_markers());
3018
3019         cpl->add (reel);
3020
3021         cpl->set_content_version (
3022                 {"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"}
3023                 );
3024         cpl->set_annotation_text ("A Test DCP");
3025         cpl->set_issuer ("OpenDCP 0.0.25");
3026         cpl->set_creator ("OpenDCP 0.0.25");
3027         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
3028         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
3029         cpl->set_main_sound_sample_rate (48000);
3030         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
3031         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
3032         cpl->set_version_number (1);
3033
3034         d.add (cpl);
3035
3036         d.set_issuer("OpenDCP 0.0.25");
3037         d.set_creator("OpenDCP 0.0.25");
3038         d.set_issue_date("2012-07-17T04:45:18+00:00");
3039         d.set_annotation_text("A Test DCP");
3040         d.write_xml(signer);
3041
3042         check_verify_result (
3043                 {dir},
3044                 {
3045                         {dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED},
3046                 });
3047 }
3048
3049
3050 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_2k)
3051 {
3052         vector<dcp::VerificationNote> notes;
3053         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"));
3054         auto reader = picture.start_read ();
3055         auto frame = reader->get_frame (0);
3056         verify_j2k (frame, notes);
3057         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3058 }
3059
3060
3061 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_4k)
3062 {
3063         vector<dcp::VerificationNote> notes;
3064         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "sul", "TLR"));
3065         auto reader = picture.start_read ();
3066         auto frame = reader->get_frame (0);
3067         verify_j2k (frame, notes);
3068         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3069 }
3070
3071
3072 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_libdcp)
3073 {
3074         boost::filesystem::path dir = "build/test/verify_jpeg2000_codestream_libdcp";
3075         prepare_directory (dir);
3076         auto dcp = make_simple (dir);
3077         dcp->write_xml ();
3078         vector<dcp::VerificationNote> notes;
3079         dcp::MonoPictureAsset picture (find_file(dir, "video"));
3080         auto reader = picture.start_read ();
3081         auto frame = reader->get_frame (0);
3082         verify_j2k (frame, notes);
3083         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3084 }
3085
3086
3087 /** Check that ResourceID and the XML ID being different is spotted */
3088 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_resource_id)
3089 {
3090         boost::filesystem::path const dir = "build/test/verify_mismatched_subtitle_resource_id";
3091         prepare_directory (dir);
3092
3093         ASDCP::WriterInfo writer_info;
3094         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3095
3096         unsigned int c;
3097         auto mxf_id = dcp::make_uuid ();
3098         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3099         BOOST_REQUIRE (c == Kumu::UUID_Length);
3100
3101         auto resource_id = dcp::make_uuid ();
3102         ASDCP::TimedText::TimedTextDescriptor descriptor;
3103         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3104         DCP_ASSERT (c == Kumu::UUID_Length);
3105
3106         auto xml_id = dcp::make_uuid ();
3107         ASDCP::TimedText::MXFWriter writer;
3108         auto subs_mxf = dir / "subs.mxf";
3109         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3110         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3111         writer.WriteTimedTextResource (dcp::String::compose(
3112                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3113                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3114                 "<Id>urn:uuid:%1</Id>"
3115                 "<ContentTitleText>Content</ContentTitleText>"
3116                 "<AnnotationText>Annotation</AnnotationText>"
3117                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3118                 "<ReelNumber>1</ReelNumber>"
3119                 "<Language>en-US</Language>"
3120                 "<EditRate>25 1</EditRate>"
3121                 "<TimeCodeRate>25</TimeCodeRate>"
3122                 "<StartTime>00:00:00:00</StartTime>"
3123                 "<SubtitleList>"
3124                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3125                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3126                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3127                 "</Subtitle>"
3128                 "</Font>"
3129                 "</SubtitleList>"
3130                 "</SubtitleReel>",
3131                 xml_id).c_str());
3132
3133         writer.Finalize();
3134
3135         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3136         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3137
3138         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3139
3140         check_verify_result (
3141                 { dir },
3142                 {
3143                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3144                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID },
3145                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3146                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3147                 });
3148 }
3149
3150
3151 /** Check that ResourceID and the MXF ID being the same is spotted */
3152 BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
3153 {
3154         boost::filesystem::path const dir = "build/test/verify_incorrect_timed_text_id";
3155         prepare_directory (dir);
3156
3157         ASDCP::WriterInfo writer_info;
3158         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3159
3160         unsigned int c;
3161         auto mxf_id = dcp::make_uuid ();
3162         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3163         BOOST_REQUIRE (c == Kumu::UUID_Length);
3164
3165         auto resource_id = mxf_id;
3166         ASDCP::TimedText::TimedTextDescriptor descriptor;
3167         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3168         DCP_ASSERT (c == Kumu::UUID_Length);
3169
3170         auto xml_id = resource_id;
3171         ASDCP::TimedText::MXFWriter writer;
3172         auto subs_mxf = dir / "subs.mxf";
3173         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3174         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3175         writer.WriteTimedTextResource (dcp::String::compose(
3176                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3177                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3178                 "<Id>urn:uuid:%1</Id>"
3179                 "<ContentTitleText>Content</ContentTitleText>"
3180                 "<AnnotationText>Annotation</AnnotationText>"
3181                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3182                 "<ReelNumber>1</ReelNumber>"
3183                 "<Language>en-US</Language>"
3184                 "<EditRate>25 1</EditRate>"
3185                 "<TimeCodeRate>25</TimeCodeRate>"
3186                 "<StartTime>00:00:00:00</StartTime>"
3187                 "<SubtitleList>"
3188                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3189                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3190                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3191                 "</Subtitle>"
3192                 "</Font>"
3193                 "</SubtitleList>"
3194                 "</SubtitleReel>",
3195                 xml_id).c_str());
3196
3197         writer.Finalize();
3198
3199         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3200         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3201
3202         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3203
3204         check_verify_result (
3205                 { dir },
3206                 {
3207                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3208                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID },
3209                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3210                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3211                 });
3212 }
3213
3214
3215 /** Check a DCP with a 3D asset marked as 2D */
3216 BOOST_AUTO_TEST_CASE (verify_threed_marked_as_twod)
3217 {
3218         check_verify_result (
3219                 { private_test / "data" / "xm" },
3220                 {
3221                         {
3222                                 dcp::VerificationNote::Type::WARNING,
3223                                 dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, boost::filesystem::canonical(find_file(private_test / "data" / "xm", "j2c"))
3224                         },
3225                         {
3226                                 dcp::VerificationNote::Type::BV21_ERROR,
3227                                 dcp::VerificationNote::Code::INVALID_STANDARD
3228                         },
3229                 });
3230
3231 }
3232
3233
3234 BOOST_AUTO_TEST_CASE (verify_unexpected_things_in_main_markers)
3235 {
3236         path dir = "build/test/verify_unexpected_things_in_main_markers";
3237         prepare_directory (dir);
3238         auto dcp = make_simple (dir, 1, 24);
3239         dcp->set_annotation_text("A Test DCP");
3240         dcp->write_xml();
3241
3242         {
3243                 Editor e (find_cpl(dir));
3244                 e.insert(
3245                         "          <IntrinsicDuration>24</IntrinsicDuration>",
3246                         "<EntryPoint>0</EntryPoint><Duration>24</Duration>"
3247                         );
3248         }
3249
3250         dcp::CPL cpl (find_cpl(dir));
3251
3252         check_verify_result (
3253                 { dir },
3254                 {
3255                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3256                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_ENTRY_POINT },
3257                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_DURATION },
3258                 });
3259 }
3260
3261
3262 BOOST_AUTO_TEST_CASE(verify_invalid_content_kind)
3263 {
3264         path dir = "build/test/verify_invalid_content_kind";
3265         prepare_directory (dir);
3266         auto dcp = make_simple (dir, 1, 24);
3267         dcp->set_annotation_text("A Test DCP");
3268         dcp->write_xml();
3269
3270         {
3271                 Editor e(find_cpl(dir));
3272                 e.replace("trailer", "trip");
3273         }
3274
3275         dcp::CPL cpl (find_cpl(dir));
3276
3277         check_verify_result (
3278                 { dir },
3279                 {
3280                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3281                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_CONTENT_KIND, string("trip") }
3282                 });
3283
3284 }
3285
3286
3287 BOOST_AUTO_TEST_CASE(verify_valid_content_kind)
3288 {
3289         path dir = "build/test/verify_valid_content_kind";
3290         prepare_directory (dir);
3291         auto dcp = make_simple (dir, 1, 24);
3292         dcp->set_annotation_text("A Test DCP");
3293         dcp->write_xml();
3294
3295         {
3296                 Editor e(find_cpl(dir));
3297                 e.replace("<ContentKind>trailer</ContentKind>", "<ContentKind scope=\"http://bobs.contents/\">trip</ContentKind>");
3298         }
3299
3300         dcp::CPL cpl (find_cpl(dir));
3301
3302         check_verify_result (
3303                 { dir },
3304                 {
3305                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3306                 });
3307
3308 }
3309
3310
3311 BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_1)
3312 {
3313         path dir = "build/test/verify_invalid_main_picture_active_area_1";
3314         prepare_directory(dir);
3315         auto dcp = make_simple(dir, 1, 24);
3316         dcp->write_xml();
3317
3318         auto constexpr area = "<meta:MainPictureActiveArea>";
3319
3320         {
3321                 Editor e(find_cpl(dir));
3322                 e.delete_lines_after(area, 2);
3323                 e.insert(area, "<meta:Height>4080</meta:Height>");
3324                 e.insert(area, "<meta:Width>1997</meta:Width>");
3325         }
3326
3327         dcp::PKL pkl(find_pkl(dir));
3328         dcp::CPL cpl(find_cpl(dir));
3329
3330         check_verify_result(
3331                 { dir },
3332                 {
3333                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3334                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3335                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "width 1997 is not a multiple of 2", canonical(find_cpl(dir)) },
3336                         { 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)) },
3337                 });
3338 }
3339
3340
3341 BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_2)
3342 {
3343         path dir = "build/test/verify_invalid_main_picture_active_area_2";
3344         prepare_directory(dir);
3345         auto dcp = make_simple(dir, 1, 24);
3346         dcp->write_xml();
3347
3348         auto constexpr area = "<meta:MainPictureActiveArea>";
3349
3350         {
3351                 Editor e(find_cpl(dir));
3352                 e.delete_lines_after(area, 2);
3353                 e.insert(area, "<meta:Height>5125</meta:Height>");
3354                 e.insert(area, "<meta:Width>9900</meta:Width>");
3355         }
3356
3357         dcp::PKL pkl(find_pkl(dir));
3358         dcp::CPL cpl(find_cpl(dir));
3359
3360         check_verify_result(
3361                 { dir },
3362                 {
3363                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3364                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3365                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 5125 is not a multiple of 2", canonical(find_cpl(dir)) },
3366                         { 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)) },
3367                         { 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)) },
3368                 });
3369 }
3370
3371
3372 BOOST_AUTO_TEST_CASE(verify_duplicate_pkl_asset_ids)
3373 {
3374         RNGFixer rg;
3375
3376         path dir = "build/test/verify_duplicate_pkl_asset_ids";
3377         prepare_directory(dir);
3378         auto dcp = make_simple(dir, 1, 24);
3379         dcp->write_xml();
3380
3381         {
3382                 Editor e(find_pkl(dir));
3383                 e.replace("urn:uuid:5407b210-4441-4e97-8b16-8bdc7c12da54", "urn:uuid:6affb8ee-0020-4dff-a53c-17652f6358ab");
3384         }
3385
3386         dcp::PKL pkl(find_pkl(dir));
3387
3388         check_verify_result(
3389                 { dir },
3390                 {
3391                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl.id(), canonical(find_pkl(dir)) },
3392                 });
3393 }
3394
3395
3396 BOOST_AUTO_TEST_CASE(verify_duplicate_assetmap_asset_ids)
3397 {
3398         RNGFixer rg;
3399
3400         path dir = "build/test/verify_duplicate_assetmap_asset_ids";
3401         prepare_directory(dir);
3402         auto dcp = make_simple(dir, 1, 24);
3403         dcp->write_xml();
3404
3405         {
3406                 Editor e(find_asset_map(dir));
3407                 e.replace("urn:uuid:5407b210-4441-4e97-8b16-8bdc7c12da54", "urn:uuid:97f0f352-5b77-48ee-a558-9df37717f4fa");
3408         }
3409
3410         dcp::PKL pkl(find_pkl(dir));
3411         dcp::AssetMap asset_map(find_asset_map(dir));
3412
3413         check_verify_result(
3414                 { dir },
3415                 {
3416                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3417                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map.id(), canonical(find_asset_map(dir)) },
3418                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, string("5407b210-4441-4e97-8b16-8bdc7c12da54") },
3419                 });
3420 }
3421