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