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