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