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