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