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