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