Properly support ContentKind scope attribute.
[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                 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                         dcp::Direction::LTR,
1260                         text,
1261                         dcp::Effect::NONE,
1262                         dcp::Colour(),
1263                         dcp::Time(),
1264                         dcp::Time(),
1265                         0
1266                 )
1267         );
1268 }
1269
1270
1271 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
1272 {
1273         path const dir("build/test/verify_invalid_closed_caption_xml_size_in_bytes");
1274         prepare_directory (dir);
1275
1276         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1277         for (int i = 0; i < 2048; ++i) {
1278                 add_test_subtitle (asset, i * 24, i * 24 + 20);
1279         }
1280         asset->set_language (dcp::LanguageTag("de-DE"));
1281         asset->write (dir / "subs.mxf");
1282         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
1283         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1284
1285         check_verify_result (
1286                 { dir },
1287                 {
1288                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1289                         {
1290                                 dcp::VerificationNote::Type::BV21_ERROR,
1291                                 dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES,
1292                                 string("419346"),
1293                                 canonical(dir / "subs.mxf")
1294                         },
1295                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1296                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1297                 });
1298 }
1299
1300
1301 static
1302 shared_ptr<dcp::SMPTESubtitleAsset>
1303 make_large_subtitle_asset (path font_file)
1304 {
1305         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1306         dcp::ArrayData big_fake_font(1024 * 1024);
1307         big_fake_font.write (font_file);
1308         for (int i = 0; i < 116; ++i) {
1309                 asset->add_font (dcp::String::compose("big%1", i), big_fake_font);
1310         }
1311         return asset;
1312 }
1313
1314
1315 template <class T>
1316 void
1317 verify_timed_text_asset_too_large (string name)
1318 {
1319         auto const dir = path("build/test") / name;
1320         prepare_directory (dir);
1321         auto asset = make_large_subtitle_asset (dir / "font.ttf");
1322         add_test_subtitle (asset, 0, 240);
1323         asset->set_language (dcp::LanguageTag("de-DE"));
1324         asset->write (dir / "subs.mxf");
1325
1326         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), 240, 0);
1327         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1328
1329         check_verify_result (
1330                 { dir },
1331                 {
1332                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121695542"), canonical(dir / "subs.mxf") },
1333                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, string("121634816"), canonical(dir / "subs.mxf") },
1334                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1335                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1336                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1337                 });
1338 }
1339
1340
1341 BOOST_AUTO_TEST_CASE (verify_subtitle_asset_too_large)
1342 {
1343         verify_timed_text_asset_too_large<dcp::ReelSMPTESubtitleAsset>("verify_subtitle_asset_too_large");
1344         verify_timed_text_asset_too_large<dcp::ReelSMPTEClosedCaptionAsset>("verify_closed_caption_asset_too_large");
1345 }
1346
1347
1348 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_language)
1349 {
1350         path dir = "build/test/verify_missing_subtitle_language";
1351         prepare_directory (dir);
1352         auto dcp = make_simple (dir, 1, 106);
1353
1354         string const xml =
1355                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1356                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1357                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1358                 "<ContentTitleText>Content</ContentTitleText>"
1359                 "<AnnotationText>Annotation</AnnotationText>"
1360                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1361                 "<ReelNumber>1</ReelNumber>"
1362                 "<EditRate>24 1</EditRate>"
1363                 "<TimeCodeRate>24</TimeCodeRate>"
1364                 "<StartTime>00:00:00:00</StartTime>"
1365                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1366                 "<SubtitleList>"
1367                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1368                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1369                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1370                 "</Subtitle>"
1371                 "</Font>"
1372                 "</SubtitleList>"
1373                 "</SubtitleReel>";
1374
1375         dcp::File xml_file(dir / "subs.xml", "w");
1376         BOOST_REQUIRE (xml_file);
1377         xml_file.write(xml.c_str(), xml.size(), 1);
1378         xml_file.close();
1379         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1380         subs->write (dir / "subs.mxf");
1381
1382         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1383         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1384         dcp->set_annotation_text("A Test DCP");
1385         dcp->write_xml();
1386
1387         check_verify_result (
1388                 { dir },
1389                 {
1390                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
1391                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1392                 });
1393 }
1394
1395
1396 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_languages)
1397 {
1398         path path ("build/test/verify_mismatched_subtitle_languages");
1399         auto constexpr reel_length = 192;
1400         auto dcp = make_simple (path, 2, reel_length);
1401         auto cpl = dcp->cpls()[0];
1402
1403         {
1404                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1405                 subs->set_language (dcp::LanguageTag("de-DE"));
1406                 subs->add (simple_subtitle());
1407                 subs->write (path / "subs1.mxf");
1408                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1409                 cpl->reels()[0]->add(reel_subs);
1410         }
1411
1412         {
1413                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1414                 subs->set_language (dcp::LanguageTag("en-US"));
1415                 subs->add (simple_subtitle());
1416                 subs->write (path / "subs2.mxf");
1417                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1418                 cpl->reels()[1]->add(reel_subs);
1419         }
1420
1421         dcp->set_annotation_text("A Test DCP");
1422         dcp->write_xml();
1423
1424         check_verify_result (
1425                 { path },
1426                 {
1427                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1428                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") },
1429                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES }
1430                 });
1431 }
1432
1433
1434 BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
1435 {
1436         path path ("build/test/verify_multiple_closed_caption_languages_allowed");
1437         auto constexpr reel_length = 192;
1438         auto dcp = make_simple (path, 2, reel_length);
1439         auto cpl = dcp->cpls()[0];
1440
1441         {
1442                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1443                 ccaps->set_language (dcp::LanguageTag("de-DE"));
1444                 ccaps->add (simple_subtitle());
1445                 ccaps->write (path / "subs1.mxf");
1446                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1447                 cpl->reels()[0]->add(reel_ccaps);
1448         }
1449
1450         {
1451                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1452                 ccaps->set_language (dcp::LanguageTag("en-US"));
1453                 ccaps->add (simple_subtitle());
1454                 ccaps->write (path / "subs2.mxf");
1455                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1456                 cpl->reels()[1]->add(reel_ccaps);
1457         }
1458
1459         dcp->set_annotation_text("A Test DCP");
1460         dcp->write_xml();
1461
1462         check_verify_result (
1463                 { path },
1464                 {
1465                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1466                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") }
1467                 });
1468 }
1469
1470
1471 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_start_time)
1472 {
1473         path dir = "build/test/verify_missing_subtitle_start_time";
1474         prepare_directory (dir);
1475         auto dcp = make_simple (dir, 1, 106);
1476
1477         string const xml =
1478                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1479                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1480                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1481                 "<ContentTitleText>Content</ContentTitleText>"
1482                 "<AnnotationText>Annotation</AnnotationText>"
1483                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1484                 "<ReelNumber>1</ReelNumber>"
1485                 "<Language>de-DE</Language>"
1486                 "<EditRate>24 1</EditRate>"
1487                 "<TimeCodeRate>24</TimeCodeRate>"
1488                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1489                 "<SubtitleList>"
1490                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1491                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1492                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1493                 "</Subtitle>"
1494                 "</Font>"
1495                 "</SubtitleList>"
1496                 "</SubtitleReel>";
1497
1498         dcp::File xml_file(dir / "subs.xml", "w");
1499         BOOST_REQUIRE (xml_file);
1500         xml_file.write(xml.c_str(), xml.size(), 1);
1501         xml_file.close();
1502         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1503         subs->write (dir / "subs.mxf");
1504
1505         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1506         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1507         dcp->set_annotation_text("A Test DCP");
1508         dcp->write_xml();
1509
1510         check_verify_result (
1511                 { dir },
1512                 {
1513                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1514                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1515                 });
1516 }
1517
1518
1519 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_start_time)
1520 {
1521         path dir = "build/test/verify_invalid_subtitle_start_time";
1522         prepare_directory (dir);
1523         auto dcp = make_simple (dir, 1, 106);
1524
1525         string const xml =
1526                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1527                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1528                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1529                 "<ContentTitleText>Content</ContentTitleText>"
1530                 "<AnnotationText>Annotation</AnnotationText>"
1531                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1532                 "<ReelNumber>1</ReelNumber>"
1533                 "<Language>de-DE</Language>"
1534                 "<EditRate>24 1</EditRate>"
1535                 "<TimeCodeRate>24</TimeCodeRate>"
1536                 "<StartTime>00:00:02:00</StartTime>"
1537                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1538                 "<SubtitleList>"
1539                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1540                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1541                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1542                 "</Subtitle>"
1543                 "</Font>"
1544                 "</SubtitleList>"
1545                 "</SubtitleReel>";
1546
1547         dcp::File xml_file(dir / "subs.xml", "w");
1548         BOOST_REQUIRE (xml_file);
1549         xml_file.write(xml.c_str(), xml.size(), 1);
1550         xml_file.close();
1551         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1552         subs->write (dir / "subs.mxf");
1553
1554         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1555         dcp->cpls().front()->reels().front()->add(reel_subs);
1556         dcp->set_annotation_text("A Test DCP");
1557         dcp->write_xml();
1558
1559         check_verify_result (
1560                 { dir },
1561                 {
1562                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1563                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1564                 });
1565 }
1566
1567
1568 class TestText
1569 {
1570 public:
1571         TestText (int in_, int out_, float v_position_ = 0, dcp::VAlign v_align_ = dcp::VAlign::CENTER, string text_ = "Hello")
1572                 : in(in_)
1573                 , out(out_)
1574                 , v_position(v_position_)
1575                 , v_align(v_align_)
1576                 , text(text_)
1577         {}
1578
1579         int in;
1580         int out;
1581         float v_position;
1582         dcp::VAlign v_align;
1583         string text;
1584 };
1585
1586
1587 template <class T>
1588 shared_ptr<dcp::CPL>
1589 dcp_with_text (path dir, vector<TestText> subs)
1590 {
1591         prepare_directory (dir);
1592         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1593         asset->set_start_time (dcp::Time());
1594         for (auto i: subs) {
1595                 add_test_subtitle (asset, i.in, i.out, i.v_position, i.v_align, i.text);
1596         }
1597         asset->set_language (dcp::LanguageTag("de-DE"));
1598         asset->write (dir / "subs.mxf");
1599
1600         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1601         return write_dcp_with_single_asset (dir, reel_asset);
1602 }
1603
1604
1605 template <class T>
1606 shared_ptr<dcp::CPL>
1607 dcp_with_text_from_file (path dir, boost::filesystem::path subs_xml)
1608 {
1609         prepare_directory (dir);
1610         auto asset = make_shared<dcp::SMPTESubtitleAsset>(subs_xml);
1611         asset->set_start_time (dcp::Time());
1612         asset->set_language (dcp::LanguageTag("de-DE"));
1613
1614         auto subs_mxf = dir / "subs.mxf";
1615         asset->write (subs_mxf);
1616
1617         /* The call to write() puts the asset into the DCP correctly but it will have
1618          * XML re-written by our parser.  Overwrite the MXF using the given file's verbatim
1619          * contents.
1620          */
1621         ASDCP::TimedText::MXFWriter writer;
1622         ASDCP::WriterInfo writer_info;
1623         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
1624         unsigned int c;
1625         Kumu::hex2bin (asset->id().c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
1626         DCP_ASSERT (c == Kumu::UUID_Length);
1627         ASDCP::TimedText::TimedTextDescriptor descriptor;
1628         descriptor.ContainerDuration = asset->intrinsic_duration();
1629         Kumu::hex2bin (asset->xml_id()->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
1630         DCP_ASSERT (c == Kumu::UUID_Length);
1631         ASDCP::Result_t r = writer.OpenWrite (subs_mxf.string().c_str(), writer_info, descriptor, 16384);
1632         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1633         r = writer.WriteTimedTextResource (dcp::file_to_string(subs_xml));
1634         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1635         writer.Finalize ();
1636
1637         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1638         return write_dcp_with_single_asset (dir, reel_asset);
1639 }
1640
1641
1642 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
1643 {
1644         auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
1645         /* Just too early */
1646         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
1647         check_verify_result (
1648                 { dir },
1649                 {
1650                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1651                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1652                 });
1653
1654 }
1655
1656
1657 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time)
1658 {
1659         auto const dir = path("build/test/verify_valid_subtitle_first_text_time");
1660         /* Just late enough */
1661         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
1662         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1663 }
1664
1665
1666 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time_on_second_reel)
1667 {
1668         auto const dir = path("build/test/verify_valid_subtitle_first_text_time_on_second_reel");
1669         prepare_directory (dir);
1670
1671         auto asset1 = make_shared<dcp::SMPTESubtitleAsset>();
1672         asset1->set_start_time (dcp::Time());
1673         /* Just late enough */
1674         add_test_subtitle (asset1, 4 * 24, 5 * 24);
1675         asset1->set_language (dcp::LanguageTag("de-DE"));
1676         asset1->write (dir / "subs1.mxf");
1677         auto reel_asset1 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset1, dcp::Fraction(24, 1), 5 * 24, 0);
1678         auto reel1 = make_shared<dcp::Reel>();
1679         reel1->add (reel_asset1);
1680         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 5 * 24);
1681         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
1682         reel1->add (markers1);
1683
1684         auto asset2 = make_shared<dcp::SMPTESubtitleAsset>();
1685         asset2->set_start_time (dcp::Time());
1686         /* This would be too early on first reel but should be OK on the second */
1687         add_test_subtitle (asset2, 3, 4 * 24);
1688         asset2->set_language (dcp::LanguageTag("de-DE"));
1689         asset2->write (dir / "subs2.mxf");
1690         auto reel_asset2 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset2, dcp::Fraction(24, 1), 4 * 24, 0);
1691         auto reel2 = make_shared<dcp::Reel>();
1692         reel2->add (reel_asset2);
1693         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 4 * 24);
1694         markers2->set (dcp::Marker::LFOC, dcp::Time(4 * 24 - 1, 24, 24));
1695         reel2->add (markers2);
1696
1697         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1698         cpl->add (reel1);
1699         cpl->add (reel2);
1700         auto dcp = make_shared<dcp::DCP>(dir);
1701         dcp->add (cpl);
1702         dcp->set_annotation_text("hello");
1703         dcp->write_xml();
1704
1705         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1706 }
1707
1708
1709 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_spacing)
1710 {
1711         auto const dir = path("build/test/verify_invalid_subtitle_spacing");
1712         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1713                 dir,
1714                 {
1715                         { 4 * 24,     5 * 24 },
1716                         { 5 * 24 + 1, 6 * 24 },
1717                 });
1718         check_verify_result (
1719                 {dir},
1720                 {
1721                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING },
1722                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1723                 });
1724 }
1725
1726
1727 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_spacing)
1728 {
1729         auto const dir = path("build/test/verify_valid_subtitle_spacing");
1730         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1731                 dir,
1732                 {
1733                         { 4 * 24,      5 * 24 },
1734                         { 5 * 24 + 16, 8 * 24 },
1735                 });
1736         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1737 }
1738
1739
1740 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_duration)
1741 {
1742         auto const dir = path("build/test/verify_invalid_subtitle_duration");
1743         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
1744         check_verify_result (
1745                 {dir},
1746                 {
1747                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION },
1748                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1749                 });
1750 }
1751
1752
1753 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_duration)
1754 {
1755         auto const dir = path("build/test/verify_valid_subtitle_duration");
1756         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
1757         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1758 }
1759
1760
1761 BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
1762 {
1763         auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
1764         prepare_directory (dir);
1765         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1766         asset->set_start_time (dcp::Time());
1767         add_test_subtitle (asset, 0, 4 * 24);
1768         asset->set_language (dcp::LanguageTag("de-DE"));
1769         asset->write (dir / "subs.mxf");
1770
1771         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
1772         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1773         check_verify_result (
1774                 {dir},
1775                 {
1776                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "72 96", boost::filesystem::canonical(asset->file().get()) },
1777                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1778                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
1779                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1780                 });
1781
1782 }
1783
1784
1785 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
1786 {
1787         auto const dir = path ("build/test/invalid_subtitle_line_count1");
1788         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1789                 dir,
1790                 {
1791                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1792                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1793                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1794                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1795                 });
1796         check_verify_result (
1797                 {dir},
1798                 {
1799                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1800                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1801                 });
1802 }
1803
1804
1805 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
1806 {
1807         auto const dir = path ("build/test/verify_valid_subtitle_line_count1");
1808         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1809                 dir,
1810                 {
1811                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1812                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1813                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1814                 });
1815         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1816 }
1817
1818
1819 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
1820 {
1821         auto const dir = path ("build/test/verify_invalid_subtitle_line_count2");
1822         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1823                 dir,
1824                 {
1825                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1826                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1827                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1828                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
1829                 });
1830         check_verify_result (
1831                 {dir},
1832                 {
1833                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1834                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1835                 });
1836 }
1837
1838
1839 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
1840 {
1841         auto const dir = path ("build/test/verify_valid_subtitle_line_count2");
1842         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1843                 dir,
1844                 {
1845                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1846                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1847                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1848                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
1849                 });
1850         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1851 }
1852
1853
1854 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
1855 {
1856         auto const dir = path ("build/test/verify_invalid_subtitle_line_length1");
1857         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1858                 dir,
1859                 {
1860                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123" }
1861                 });
1862         check_verify_result (
1863                 {dir},
1864                 {
1865                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH },
1866                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1867                 });
1868 }
1869
1870
1871 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
1872 {
1873         auto const dir = path ("build/test/verify_invalid_subtitle_line_length2");
1874         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1875                 dir,
1876                 {
1877                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
1878                 });
1879         check_verify_result (
1880                 {dir},
1881                 {
1882                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH },
1883                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1884                 });
1885 }
1886
1887
1888 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
1889 {
1890         auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
1891         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1892                 dir,
1893                 {
1894                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1895                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1896                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1897                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1898                 });
1899         check_verify_result (
1900                 {dir},
1901                 {
1902                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1903                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1904                 });
1905 }
1906
1907
1908 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
1909 {
1910         auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
1911         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1912                 dir,
1913                 {
1914                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1915                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1916                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1917                 });
1918         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1919 }
1920
1921
1922 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
1923 {
1924         auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
1925         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1926                 dir,
1927                 {
1928                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1929                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1930                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1931                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
1932                 });
1933         check_verify_result (
1934                 {dir},
1935                 {
1936                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1937                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1938                 });
1939 }
1940
1941
1942 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
1943 {
1944         auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
1945         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1946                 dir,
1947                 {
1948                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1949                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1950                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1951                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
1952                 });
1953         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1954 }
1955
1956
1957 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_length)
1958 {
1959         auto const dir = path ("build/test/verify_valid_closed_caption_line_length");
1960         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1961                 dir,
1962                 {
1963                         { 96, 300, 0.0, dcp::VAlign::CENTER, "01234567890123456789012345678901" }
1964                 });
1965         check_verify_result (
1966                 {dir},
1967                 {
1968                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1969                 });
1970 }
1971
1972
1973 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
1974 {
1975         auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
1976         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1977                 dir,
1978                 {
1979                         { 96, 300, 0.0, dcp::VAlign::CENTER, "0123456789012345678901234567890123" }
1980                 });
1981         check_verify_result (
1982                 {dir},
1983                 {
1984                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH },
1985                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1986                 });
1987 }
1988
1989
1990 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign1)
1991 {
1992         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign1");
1993         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1994                 dir,
1995                 {
1996                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
1997                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
1998                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
1999                 });
2000         check_verify_result (
2001                 {dir},
2002                 {
2003                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2004                 });
2005 }
2006
2007
2008 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign2)
2009 {
2010         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign2");
2011         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2012                 dir,
2013                 {
2014                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2015                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2016                         { 96, 300, 0.2, dcp::VAlign::CENTER, "not fine" },
2017                 });
2018         check_verify_result (
2019                 {dir},
2020                 {
2021                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN },
2022                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2023                 });
2024 }
2025
2026
2027 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering1)
2028 {
2029         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering1");
2030         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2031                 dir,
2032                 {
2033                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2034                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2035                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2036                 });
2037         check_verify_result (
2038                 {dir},
2039                 {
2040                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2041                 });
2042 }
2043
2044
2045 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering2)
2046 {
2047         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering2");
2048         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2049                 dir,
2050                 {
2051                         { 96, 300, 0.2, dcp::VAlign::BOTTOM, "This" },
2052                         { 96, 300, 0.1, dcp::VAlign::BOTTOM, "is" },
2053                         { 96, 300, 0.0, dcp::VAlign::BOTTOM, "also fine" },
2054                 });
2055         check_verify_result (
2056                 {dir},
2057                 {
2058                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2059                 });
2060 }
2061
2062
2063 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering3)
2064 {
2065         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering3");
2066         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering3.xml");
2067         check_verify_result (
2068                 {dir},
2069                 {
2070                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING },
2071                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2072                 });
2073 }
2074
2075
2076 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering4)
2077 {
2078         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering4");
2079         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering4.xml");
2080         check_verify_result (
2081                 {dir},
2082                 {
2083                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2084                 });
2085 }
2086
2087
2088
2089 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
2090 {
2091         path const dir("build/test/verify_invalid_sound_frame_rate");
2092         prepare_directory (dir);
2093
2094         auto picture = simple_picture (dir, "foo");
2095         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
2096         auto reel = make_shared<dcp::Reel>();
2097         reel->add (reel_picture);
2098         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000, boost::none);
2099         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
2100         reel->add (reel_sound);
2101         reel->add (simple_markers());
2102         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2103         cpl->add (reel);
2104         auto dcp = make_shared<dcp::DCP>(dir);
2105         dcp->add (cpl);
2106         dcp->set_annotation_text("hello");
2107         dcp->write_xml();
2108
2109         check_verify_result (
2110                 {dir},
2111                 {
2112                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE, string("96000"), canonical(dir / "audiofoo.mxf") },
2113                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2114                 });
2115 }
2116
2117
2118 BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
2119 {
2120         path const dir("build/test/verify_missing_cpl_annotation_text");
2121         auto dcp = make_simple (dir);
2122         dcp->set_annotation_text("A Test DCP");
2123         dcp->write_xml();
2124
2125         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2126
2127         auto const cpl = dcp->cpls()[0];
2128
2129         {
2130                 BOOST_REQUIRE (cpl->file());
2131                 Editor e(cpl->file().get());
2132                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "");
2133         }
2134
2135         check_verify_result (
2136                 {dir},
2137                 {
2138                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2139                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2140                 });
2141 }
2142
2143
2144 BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
2145 {
2146         path const dir("build/test/verify_mismatched_cpl_annotation_text");
2147         auto dcp = make_simple (dir);
2148         dcp->set_annotation_text("A Test DCP");
2149         dcp->write_xml();
2150
2151         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2152         auto const cpl = dcp->cpls()[0];
2153
2154         {
2155                 BOOST_REQUIRE (cpl->file());
2156                 Editor e(cpl->file().get());
2157                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "<AnnotationText>A Test DCP 1</AnnotationText>");
2158         }
2159
2160         check_verify_result (
2161                 {dir},
2162                 {
2163                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2164                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2165                 });
2166 }
2167
2168
2169 BOOST_AUTO_TEST_CASE (verify_mismatched_asset_duration)
2170 {
2171         path const dir("build/test/verify_mismatched_asset_duration");
2172         prepare_directory (dir);
2173         shared_ptr<dcp::DCP> dcp (new dcp::DCP(dir));
2174         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2175
2176         shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (dir, "", 24);
2177         shared_ptr<dcp::SoundAsset> ms = simple_sound (dir, "", dcp::MXFMetadata(), "en-US", 25);
2178
2179         auto reel = make_shared<dcp::Reel>(
2180                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2181                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2182                 );
2183
2184         reel->add (simple_markers());
2185         cpl->add (reel);
2186
2187         dcp->add (cpl);
2188         dcp->set_annotation_text("A Test DCP");
2189         dcp->write_xml();
2190
2191         check_verify_result (
2192                 {dir},
2193                 {
2194                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION },
2195                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), canonical(cpl->file().get()) }
2196                 });
2197 }
2198
2199
2200
2201 static
2202 shared_ptr<dcp::CPL>
2203 verify_subtitles_must_be_in_all_reels_check (path dir, bool add_to_reel1, bool add_to_reel2)
2204 {
2205         prepare_directory (dir);
2206         auto dcp = make_shared<dcp::DCP>(dir);
2207         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2208
2209         auto constexpr reel_length = 192;
2210
2211         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2212         subs->set_language (dcp::LanguageTag("de-DE"));
2213         subs->set_start_time (dcp::Time());
2214         subs->add (simple_subtitle());
2215         subs->write (dir / "subs.mxf");
2216         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
2217
2218         auto reel1 = make_shared<dcp::Reel>(
2219                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2220                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2221                 );
2222
2223         if (add_to_reel1) {
2224                 reel1->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2225         }
2226
2227         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2228         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2229         reel1->add (markers1);
2230
2231         cpl->add (reel1);
2232
2233         auto reel2 = make_shared<dcp::Reel>(
2234                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2235                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2236                 );
2237
2238         if (add_to_reel2) {
2239                 reel2->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2240         }
2241
2242         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2243         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2244         reel2->add (markers2);
2245
2246         cpl->add (reel2);
2247
2248         dcp->add (cpl);
2249         dcp->set_annotation_text("A Test DCP");
2250         dcp->write_xml();
2251
2252         return cpl;
2253 }
2254
2255
2256 BOOST_AUTO_TEST_CASE (verify_missing_main_subtitle_from_some_reels)
2257 {
2258         {
2259                 path dir ("build/test/missing_main_subtitle_from_some_reels");
2260                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, false);
2261                 check_verify_result (
2262                         { dir },
2263                         {
2264                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS },
2265                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2266                         });
2267
2268         }
2269
2270         {
2271                 path dir ("build/test/verify_subtitles_must_be_in_all_reels2");
2272                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, true);
2273                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2274         }
2275
2276         {
2277                 path dir ("build/test/verify_subtitles_must_be_in_all_reels1");
2278                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, false, false);
2279                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2280         }
2281 }
2282
2283
2284 static
2285 shared_ptr<dcp::CPL>
2286 verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1, int caps_in_reel2)
2287 {
2288         prepare_directory (dir);
2289         auto dcp = make_shared<dcp::DCP>(dir);
2290         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2291
2292         auto constexpr reel_length = 192;
2293
2294         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2295         subs->set_language (dcp::LanguageTag("de-DE"));
2296         subs->set_start_time (dcp::Time());
2297         subs->add (simple_subtitle());
2298         subs->write (dir / "subs.mxf");
2299
2300         auto reel1 = make_shared<dcp::Reel>(
2301                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2302                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2303                 );
2304
2305         for (int i = 0; i < caps_in_reel1; ++i) {
2306                 reel1->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2307         }
2308
2309         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2310         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2311         reel1->add (markers1);
2312
2313         cpl->add (reel1);
2314
2315         auto reel2 = make_shared<dcp::Reel>(
2316                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2317                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2318                 );
2319
2320         for (int i = 0; i < caps_in_reel2; ++i) {
2321                 reel2->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2322         }
2323
2324         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2325         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2326         reel2->add (markers2);
2327
2328         cpl->add (reel2);
2329
2330         dcp->add (cpl);
2331         dcp->set_annotation_text("A Test DCP");
2332         dcp->write_xml();
2333
2334         return cpl;
2335 }
2336
2337
2338 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_asset_counts)
2339 {
2340         {
2341                 path dir ("build/test/mismatched_closed_caption_asset_counts");
2342                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 3, 4);
2343                 check_verify_result (
2344                         {dir},
2345                         {
2346                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS },
2347                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2348                         });
2349         }
2350
2351         {
2352                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels2");
2353                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 4, 4);
2354                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2355         }
2356
2357         {
2358                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels3");
2359                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 0, 0);
2360                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2361         }
2362 }
2363
2364
2365 template <class T>
2366 void
2367 verify_text_entry_point_check (path dir, dcp::VerificationNote::Code code, boost::function<void (shared_ptr<T>)> adjust)
2368 {
2369         prepare_directory (dir);
2370         auto dcp = make_shared<dcp::DCP>(dir);
2371         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2372
2373         auto constexpr reel_length = 192;
2374
2375         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2376         subs->set_language (dcp::LanguageTag("de-DE"));
2377         subs->set_start_time (dcp::Time());
2378         subs->add (simple_subtitle());
2379         subs->write (dir / "subs.mxf");
2380         auto reel_text = make_shared<T>(subs, dcp::Fraction(24, 1), reel_length, 0);
2381         adjust (reel_text);
2382
2383         auto reel = make_shared<dcp::Reel>(
2384                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2385                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2386                 );
2387
2388         reel->add (reel_text);
2389
2390         reel->add (simple_markers(reel_length));
2391
2392         cpl->add (reel);
2393
2394         dcp->add (cpl);
2395         dcp->set_annotation_text("A Test DCP");
2396         dcp->write_xml();
2397
2398         check_verify_result (
2399                 {dir},
2400                 {
2401                         { dcp::VerificationNote::Type::BV21_ERROR, code, subs->id() },
2402                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2403                 });
2404 }
2405
2406
2407 BOOST_AUTO_TEST_CASE (verify_text_entry_point)
2408 {
2409         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2410                 "build/test/verify_subtitle_entry_point_must_be_present",
2411                 dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT,
2412                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2413                         asset->unset_entry_point ();
2414                         }
2415                 );
2416
2417         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2418                 "build/test/verify_subtitle_entry_point_must_be_zero",
2419                 dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT,
2420                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2421                         asset->set_entry_point (4);
2422                         }
2423                 );
2424
2425         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2426                 "build/test/verify_closed_caption_entry_point_must_be_present",
2427                 dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
2428                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2429                         asset->unset_entry_point ();
2430                         }
2431                 );
2432
2433         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2434                 "build/test/verify_closed_caption_entry_point_must_be_zero",
2435                 dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
2436                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2437                         asset->set_entry_point (9);
2438                         }
2439                 );
2440 }
2441
2442
2443 BOOST_AUTO_TEST_CASE (verify_missing_hash)
2444 {
2445         RNGFixer fix;
2446
2447         path const dir("build/test/verify_missing_hash");
2448         auto dcp = make_simple (dir);
2449         dcp->set_annotation_text("A Test DCP");
2450         dcp->write_xml();
2451
2452         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2453         auto const cpl = dcp->cpls()[0];
2454         BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
2455         BOOST_REQUIRE (cpl->reels()[0]->main_picture());
2456         auto asset_id = cpl->reels()[0]->main_picture()->id();
2457
2458         {
2459                 BOOST_REQUIRE (cpl->file());
2460                 Editor e(cpl->file().get());
2461                 e.delete_first_line_containing("<Hash>");
2462         }
2463
2464         check_verify_result (
2465                 {dir},
2466                 {
2467                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2468                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
2469                 });
2470 }
2471
2472
2473 static
2474 void
2475 verify_markers_test (
2476         path dir,
2477         vector<pair<dcp::Marker, dcp::Time>> markers,
2478         vector<dcp::VerificationNote> test_notes
2479         )
2480 {
2481         auto dcp = make_simple (dir);
2482         dcp->cpls()[0]->set_content_kind (dcp::ContentKind::FEATURE);
2483         auto markers_asset = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 24);
2484         for (auto const& i: markers) {
2485                 markers_asset->set (i.first, i.second);
2486         }
2487         dcp->cpls()[0]->reels()[0]->add(markers_asset);
2488         dcp->set_annotation_text("A Test DCP");
2489         dcp->write_xml();
2490
2491         check_verify_result ({dir}, test_notes);
2492 }
2493
2494
2495 BOOST_AUTO_TEST_CASE (verify_markers)
2496 {
2497         verify_markers_test (
2498                 "build/test/verify_markers_all_correct",
2499                 {
2500                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2501                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2502                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2503                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2504                 },
2505                 {}
2506                 );
2507
2508         verify_markers_test (
2509                 "build/test/verify_markers_missing_ffec",
2510                 {
2511                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2512                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2513                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2514                 },
2515                 {
2516                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE }
2517                 });
2518
2519         verify_markers_test (
2520                 "build/test/verify_markers_missing_ffmc",
2521                 {
2522                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2523                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2524                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2525                 },
2526                 {
2527                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
2528                 });
2529
2530         verify_markers_test (
2531                 "build/test/verify_markers_missing_ffoc",
2532                 {
2533                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2534                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2535                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2536                 },
2537                 {
2538                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC}
2539                 });
2540
2541         verify_markers_test (
2542                 "build/test/verify_markers_missing_lfoc",
2543                 {
2544                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2545                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2546                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) }
2547                 },
2548                 {
2549                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC }
2550                 });
2551
2552         verify_markers_test (
2553                 "build/test/verify_markers_incorrect_ffoc",
2554                 {
2555                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2556                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2557                         { dcp::Marker::FFOC, dcp::Time(3, 24, 24) },
2558                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2559                 },
2560                 {
2561                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_FFOC, string("3") }
2562                 });
2563
2564         verify_markers_test (
2565                 "build/test/verify_markers_incorrect_lfoc",
2566                 {
2567                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2568                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2569                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2570                         { dcp::Marker::LFOC, dcp::Time(18, 24, 24) }
2571                 },
2572                 {
2573                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_LFOC, string("18") }
2574                 });
2575 }
2576
2577
2578 BOOST_AUTO_TEST_CASE (verify_missing_cpl_metadata_version_number)
2579 {
2580         path dir = "build/test/verify_missing_cpl_metadata_version_number";
2581         prepare_directory (dir);
2582         auto dcp = make_simple (dir);
2583         auto cpl = dcp->cpls()[0];
2584         cpl->unset_version_number();
2585         dcp->set_annotation_text("A Test DCP");
2586         dcp->write_xml();
2587
2588         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get() }});
2589 }
2590
2591
2592 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
2593 {
2594         path dir = "build/test/verify_missing_extension_metadata1";
2595         auto dcp = make_simple (dir);
2596         dcp->set_annotation_text("A Test DCP");
2597         dcp->write_xml();
2598
2599         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2600         auto cpl = dcp->cpls()[0];
2601
2602         {
2603                 Editor e (cpl->file().get());
2604                 e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
2605         }
2606
2607         check_verify_result (
2608                 {dir},
2609                 {
2610                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2611                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2612                 });
2613 }
2614
2615
2616 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
2617 {
2618         path dir = "build/test/verify_missing_extension_metadata2";
2619         auto dcp = make_simple (dir);
2620         dcp->set_annotation_text("A Test DCP");
2621         dcp->write_xml();
2622
2623         auto cpl = dcp->cpls()[0];
2624
2625         {
2626                 Editor e (cpl->file().get());
2627                 e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
2628         }
2629
2630         check_verify_result (
2631                 {dir},
2632                 {
2633                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2634                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2635                 });
2636 }
2637
2638
2639 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
2640 {
2641         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata3";
2642         auto dcp = make_simple (dir);
2643         dcp->set_annotation_text("A Test DCP");
2644         dcp->write_xml();
2645
2646         auto const cpl = dcp->cpls()[0];
2647
2648         {
2649                 Editor e (cpl->file().get());
2650                 e.replace ("<meta:Name>A", "<meta:NameX>A");
2651                 e.replace ("n</meta:Name>", "n</meta:NameX>");
2652         }
2653
2654         check_verify_result (
2655                 {dir},
2656                 {
2657                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:NameX'"), cpl->file().get(), 70 },
2658                         { 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 },
2659                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2660                 });
2661 }
2662
2663
2664 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
2665 {
2666         path dir = "build/test/verify_invalid_extension_metadata1";
2667         auto dcp = make_simple (dir);
2668         dcp->set_annotation_text("A Test DCP");
2669         dcp->write_xml();
2670
2671         auto cpl = dcp->cpls()[0];
2672
2673         {
2674                 Editor e (cpl->file().get());
2675                 e.replace ("Application", "Fred");
2676         }
2677
2678         check_verify_result (
2679                 {dir},
2680                 {
2681                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2682                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
2683                 });
2684 }
2685
2686
2687 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
2688 {
2689         path dir = "build/test/verify_invalid_extension_metadata2";
2690         auto dcp = make_simple (dir);
2691         dcp->set_annotation_text("A Test DCP");
2692         dcp->write_xml();
2693
2694         auto cpl = dcp->cpls()[0];
2695
2696         {
2697                 Editor e (cpl->file().get());
2698                 e.replace ("DCP Constraints Profile", "Fred");
2699         }
2700
2701         check_verify_result (
2702                 {dir},
2703                 {
2704                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2705                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
2706                 });
2707 }
2708
2709
2710 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
2711 {
2712         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata6";
2713         auto dcp = make_simple (dir);
2714         dcp->set_annotation_text("A Test DCP");
2715         dcp->write_xml();
2716
2717         auto const cpl = dcp->cpls()[0];
2718
2719         {
2720                 Editor e (cpl->file().get());
2721                 e.replace ("<meta:Value>", "<meta:ValueX>");
2722                 e.replace ("</meta:Value>", "</meta:ValueX>");
2723         }
2724
2725         check_verify_result (
2726                 {dir},
2727                 {
2728                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 74 },
2729                         { 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 },
2730                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2731                 });
2732 }
2733
2734
2735 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
2736 {
2737         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata7";
2738         auto dcp = make_simple (dir);
2739         dcp->set_annotation_text("A Test DCP");
2740         dcp->write_xml();
2741
2742         auto const cpl = dcp->cpls()[0];
2743
2744         {
2745                 Editor e (cpl->file().get());
2746                 e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
2747         }
2748
2749         check_verify_result (
2750                 {dir},
2751                 {
2752                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2753                         { 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() },
2754                 });
2755 }
2756
2757
2758 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
2759 {
2760         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata8";
2761         auto dcp = make_simple (dir);
2762         dcp->set_annotation_text("A Test DCP");
2763         dcp->write_xml();
2764
2765         auto const cpl = dcp->cpls()[0];
2766
2767         {
2768                 Editor e (cpl->file().get());
2769                 e.replace ("<meta:Property>", "<meta:PropertyX>");
2770                 e.replace ("</meta:Property>", "</meta:PropertyX>");
2771         }
2772
2773         check_verify_result (
2774                 {dir},
2775                 {
2776                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 72 },
2777                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 76 },
2778                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2779                 });
2780 }
2781
2782
2783 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
2784 {
2785         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata9";
2786         auto dcp = make_simple (dir);
2787         dcp->set_annotation_text("A Test DCP");
2788         dcp->write_xml();
2789
2790         auto const cpl = dcp->cpls()[0];
2791
2792         {
2793                 Editor e (cpl->file().get());
2794                 e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
2795                 e.replace ("</meta:PropertyList>", "</meta:PropertyListX>");
2796         }
2797
2798         check_verify_result (
2799                 {dir},
2800                 {
2801                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 71 },
2802                         { 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 },
2803                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2804                 });
2805 }
2806
2807
2808
2809 BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
2810 {
2811         path dir = "build/test/verify_unsigned_cpl_with_encrypted_content";
2812         prepare_directory (dir);
2813         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2814                 copy_file (i.path(), dir / i.path().filename());
2815         }
2816
2817         path const pkl = dir / ( "pkl_" + encryption_test_pkl_id + ".xml" );
2818         path const cpl = dir / ( "cpl_" + encryption_test_cpl_id + ".xml");
2819
2820         {
2821                 Editor e (cpl);
2822                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2823         }
2824
2825         check_verify_result (
2826                 {dir},
2827                 {
2828                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id, canonical(cpl) },
2829                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl), },
2830                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2831                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2832                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2833                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2834                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2835                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, encryption_test_cpl_id, canonical(cpl) }
2836                 });
2837 }
2838
2839
2840 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_encrypted_content)
2841 {
2842         path dir = "build/test/unsigned_pkl_with_encrypted_content";
2843         prepare_directory (dir);
2844         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2845                 copy_file (i.path(), dir / i.path().filename());
2846         }
2847
2848         path const cpl = dir / ("cpl_" + encryption_test_cpl_id + ".xml");
2849         path const pkl = dir / ("pkl_" + encryption_test_pkl_id + ".xml");
2850         {
2851                 Editor e (pkl);
2852                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2853         }
2854
2855         check_verify_result (
2856                 {dir},
2857                 {
2858                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl) },
2859                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2860                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2861                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2862                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2863                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2864                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, encryption_test_pkl_id, canonical(pkl) },
2865                 });
2866 }
2867
2868
2869 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_unencrypted_content)
2870 {
2871         path dir = "build/test/verify_unsigned_pkl_with_unencrypted_content";
2872         prepare_directory (dir);
2873         for (auto i: directory_iterator("test/ref/DCP/dcp_test1")) {
2874                 copy_file (i.path(), dir / i.path().filename());
2875         }
2876
2877         {
2878                 Editor e (dir / dcp_test1_pkl);
2879                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2880         }
2881
2882         check_verify_result ({dir}, {});
2883 }
2884
2885
2886 BOOST_AUTO_TEST_CASE (verify_partially_encrypted)
2887 {
2888         path dir ("build/test/verify_must_not_be_partially_encrypted");
2889         prepare_directory (dir);
2890
2891         dcp::DCP d (dir);
2892
2893         auto signer = make_shared<dcp::CertificateChain>();
2894         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem")));
2895         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem")));
2896         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem")));
2897         signer->set_key (dcp::file_to_string("test/ref/crypt/leaf.key"));
2898
2899         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2900
2901         dcp::Key key;
2902
2903         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
2904         mp->set_key (key);
2905
2906         auto writer = mp->start_write (dir / "video.mxf", false);
2907         dcp::ArrayData j2c ("test/data/flat_red.j2c");
2908         for (int i = 0; i < 24; ++i) {
2909                 writer->write (j2c.data(), j2c.size());
2910         }
2911         writer->finalize ();
2912
2913         auto ms = simple_sound (dir, "", dcp::MXFMetadata(), "de-DE");
2914
2915         auto reel = make_shared<dcp::Reel>(
2916                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2917                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2918                 );
2919
2920         reel->add (simple_markers());
2921
2922         cpl->add (reel);
2923
2924         cpl->set_content_version (
2925                 {"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"}
2926                 );
2927         cpl->set_annotation_text ("A Test DCP");
2928         cpl->set_issuer ("OpenDCP 0.0.25");
2929         cpl->set_creator ("OpenDCP 0.0.25");
2930         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
2931         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
2932         cpl->set_main_sound_sample_rate (48000);
2933         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
2934         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
2935         cpl->set_version_number (1);
2936
2937         d.add (cpl);
2938
2939         d.set_issuer("OpenDCP 0.0.25");
2940         d.set_creator("OpenDCP 0.0.25");
2941         d.set_issue_date("2012-07-17T04:45:18+00:00");
2942         d.set_annotation_text("A Test DCP");
2943         d.write_xml(signer);
2944
2945         check_verify_result (
2946                 {dir},
2947                 {
2948                         {dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED},
2949                 });
2950 }
2951
2952
2953 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_2k)
2954 {
2955         vector<dcp::VerificationNote> notes;
2956         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"));
2957         auto reader = picture.start_read ();
2958         auto frame = reader->get_frame (0);
2959         verify_j2k (frame, notes);
2960         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2961 }
2962
2963
2964 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_4k)
2965 {
2966         vector<dcp::VerificationNote> notes;
2967         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "sul", "TLR"));
2968         auto reader = picture.start_read ();
2969         auto frame = reader->get_frame (0);
2970         verify_j2k (frame, notes);
2971         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2972 }
2973
2974
2975 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_libdcp)
2976 {
2977         boost::filesystem::path dir = "build/test/verify_jpeg2000_codestream_libdcp";
2978         prepare_directory (dir);
2979         auto dcp = make_simple (dir);
2980         dcp->write_xml ();
2981         vector<dcp::VerificationNote> notes;
2982         dcp::MonoPictureAsset picture (find_file(dir, "video"));
2983         auto reader = picture.start_read ();
2984         auto frame = reader->get_frame (0);
2985         verify_j2k (frame, notes);
2986         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2987 }
2988
2989
2990 /** Check that ResourceID and the XML ID being different is spotted */
2991 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_resource_id)
2992 {
2993         boost::filesystem::path const dir = "build/test/verify_mismatched_subtitle_resource_id";
2994         prepare_directory (dir);
2995
2996         ASDCP::WriterInfo writer_info;
2997         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
2998
2999         unsigned int c;
3000         auto mxf_id = dcp::make_uuid ();
3001         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3002         BOOST_REQUIRE (c == Kumu::UUID_Length);
3003
3004         auto resource_id = dcp::make_uuid ();
3005         ASDCP::TimedText::TimedTextDescriptor descriptor;
3006         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3007         DCP_ASSERT (c == Kumu::UUID_Length);
3008
3009         auto xml_id = dcp::make_uuid ();
3010         ASDCP::TimedText::MXFWriter writer;
3011         auto subs_mxf = dir / "subs.mxf";
3012         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3013         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3014         writer.WriteTimedTextResource (dcp::String::compose(
3015                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3016                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3017                 "<Id>urn:uuid:%1</Id>"
3018                 "<ContentTitleText>Content</ContentTitleText>"
3019                 "<AnnotationText>Annotation</AnnotationText>"
3020                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3021                 "<ReelNumber>1</ReelNumber>"
3022                 "<Language>en-US</Language>"
3023                 "<EditRate>25 1</EditRate>"
3024                 "<TimeCodeRate>25</TimeCodeRate>"
3025                 "<StartTime>00:00:00:00</StartTime>"
3026                 "<SubtitleList>"
3027                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3028                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3029                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3030                 "</Subtitle>"
3031                 "</Font>"
3032                 "</SubtitleList>"
3033                 "</SubtitleReel>",
3034                 xml_id).c_str());
3035
3036         writer.Finalize();
3037
3038         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3039         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3040
3041         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3042
3043         check_verify_result (
3044                 { dir },
3045                 {
3046                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3047                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID },
3048                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3049                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3050                 });
3051 }
3052
3053
3054 /** Check that ResourceID and the MXF ID being the same is spotted */
3055 BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
3056 {
3057         boost::filesystem::path const dir = "build/test/verify_incorrect_timed_text_id";
3058         prepare_directory (dir);
3059
3060         ASDCP::WriterInfo writer_info;
3061         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3062
3063         unsigned int c;
3064         auto mxf_id = dcp::make_uuid ();
3065         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3066         BOOST_REQUIRE (c == Kumu::UUID_Length);
3067
3068         auto resource_id = mxf_id;
3069         ASDCP::TimedText::TimedTextDescriptor descriptor;
3070         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3071         DCP_ASSERT (c == Kumu::UUID_Length);
3072
3073         auto xml_id = resource_id;
3074         ASDCP::TimedText::MXFWriter writer;
3075         auto subs_mxf = dir / "subs.mxf";
3076         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3077         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3078         writer.WriteTimedTextResource (dcp::String::compose(
3079                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3080                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3081                 "<Id>urn:uuid:%1</Id>"
3082                 "<ContentTitleText>Content</ContentTitleText>"
3083                 "<AnnotationText>Annotation</AnnotationText>"
3084                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3085                 "<ReelNumber>1</ReelNumber>"
3086                 "<Language>en-US</Language>"
3087                 "<EditRate>25 1</EditRate>"
3088                 "<TimeCodeRate>25</TimeCodeRate>"
3089                 "<StartTime>00:00:00:00</StartTime>"
3090                 "<SubtitleList>"
3091                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3092                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3093                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3094                 "</Subtitle>"
3095                 "</Font>"
3096                 "</SubtitleList>"
3097                 "</SubtitleReel>",
3098                 xml_id).c_str());
3099
3100         writer.Finalize();
3101
3102         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3103         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3104
3105         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3106
3107         check_verify_result (
3108                 { dir },
3109                 {
3110                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3111                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID },
3112                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3113                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3114                 });
3115 }
3116
3117
3118 /** Check a DCP with a 3D asset marked as 2D */
3119 BOOST_AUTO_TEST_CASE (verify_threed_marked_as_twod)
3120 {
3121         check_verify_result (
3122                 { private_test / "data" / "xm" },
3123                 {
3124                         {
3125                                 dcp::VerificationNote::Type::WARNING,
3126                                 dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, boost::filesystem::canonical(find_file(private_test / "data" / "xm", "j2c"))
3127                         },
3128                         {
3129                                 dcp::VerificationNote::Type::BV21_ERROR,
3130                                 dcp::VerificationNote::Code::INVALID_STANDARD
3131                         },
3132                 });
3133
3134 }
3135
3136
3137 BOOST_AUTO_TEST_CASE (verify_unexpected_things_in_main_markers)
3138 {
3139         path dir = "build/test/verify_unexpected_things_in_main_markers";
3140         prepare_directory (dir);
3141         auto dcp = make_simple (dir, 1, 24);
3142         dcp->set_annotation_text("A Test DCP");
3143         dcp->write_xml();
3144
3145         {
3146                 Editor e (find_cpl(dir));
3147                 e.insert(
3148                         "          <IntrinsicDuration>24</IntrinsicDuration>",
3149                         "<EntryPoint>0</EntryPoint><Duration>24</Duration>"
3150                         );
3151         }
3152
3153         dcp::CPL cpl (find_cpl(dir));
3154
3155         check_verify_result (
3156                 { dir },
3157                 {
3158                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3159                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_ENTRY_POINT },
3160                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_DURATION },
3161                 });
3162 }
3163
3164
3165 BOOST_AUTO_TEST_CASE(verify_invalid_content_kind)
3166 {
3167         path dir = "build/test/verify_invalid_content_kind";
3168         prepare_directory (dir);
3169         auto dcp = make_simple (dir, 1, 24);
3170         dcp->set_annotation_text("A Test DCP");
3171         dcp->write_xml();
3172
3173         {
3174                 Editor e(find_cpl(dir));
3175                 e.replace("trailer", "trip");
3176         }
3177
3178         dcp::CPL cpl (find_cpl(dir));
3179
3180         check_verify_result (
3181                 { dir },
3182                 {
3183                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3184                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_CONTENT_KIND, string("trip") }
3185                 });
3186
3187 }
3188
3189
3190 BOOST_AUTO_TEST_CASE(verify_valid_content_kind)
3191 {
3192         path dir = "build/test/verify_valid_content_kind";
3193         prepare_directory (dir);
3194         auto dcp = make_simple (dir, 1, 24);
3195         dcp->set_annotation_text("A Test DCP");
3196         dcp->write_xml();
3197
3198         {
3199                 Editor e(find_cpl(dir));
3200                 e.replace("<ContentKind>trailer</ContentKind>", "<ContentKind scope=\"http://bobs.contents/\">trip</ContentKind>");
3201         }
3202
3203         dcp::CPL cpl (find_cpl(dir));
3204
3205         check_verify_result (
3206                 { dir },
3207                 {
3208                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3209                 });
3210
3211 }