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