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