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