2de443e3a92a719112ca75a71db80280f862beda
[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                 )
1340         );
1341 }
1342
1343
1344 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
1345 {
1346         path const dir("build/test/verify_invalid_closed_caption_xml_size_in_bytes");
1347         prepare_directory (dir);
1348
1349         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1350         for (int i = 0; i < 2048; ++i) {
1351                 add_test_subtitle (asset, i * 24, i * 24 + 20);
1352         }
1353         add_font(asset);
1354         asset->set_language (dcp::LanguageTag("de-DE"));
1355         asset->write (dir / "subs.mxf");
1356         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
1357         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1358
1359         check_verify_result (
1360                 { dir },
1361                 {
1362                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1363                         {
1364                                 dcp::VerificationNote::Type::BV21_ERROR,
1365                                 dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES,
1366                                 string("419371"),
1367                                 canonical(dir / "subs.mxf")
1368                         },
1369                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1370                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1371                 });
1372 }
1373
1374
1375 static
1376 shared_ptr<dcp::SMPTESubtitleAsset>
1377 make_large_subtitle_asset (path font_file)
1378 {
1379         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1380         dcp::ArrayData big_fake_font(1024 * 1024);
1381         big_fake_font.write (font_file);
1382         for (int i = 0; i < 116; ++i) {
1383                 asset->add_font (dcp::String::compose("big%1", i), big_fake_font);
1384         }
1385         return asset;
1386 }
1387
1388
1389 template <class T>
1390 void
1391 verify_timed_text_asset_too_large (string name)
1392 {
1393         auto const dir = path("build/test") / name;
1394         prepare_directory (dir);
1395         auto asset = make_large_subtitle_asset (dir / "font.ttf");
1396         add_test_subtitle (asset, 0, 240);
1397         asset->set_language (dcp::LanguageTag("de-DE"));
1398         asset->write (dir / "subs.mxf");
1399
1400         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), 240, 0);
1401         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1402
1403         check_verify_result (
1404                 { dir },
1405                 {
1406                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121695488"), canonical(dir / "subs.mxf") },
1407                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, string("121634816"), canonical(dir / "subs.mxf") },
1408                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1409                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1410                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1411                 });
1412 }
1413
1414
1415 BOOST_AUTO_TEST_CASE (verify_subtitle_asset_too_large)
1416 {
1417         verify_timed_text_asset_too_large<dcp::ReelSMPTESubtitleAsset>("verify_subtitle_asset_too_large");
1418         verify_timed_text_asset_too_large<dcp::ReelSMPTEClosedCaptionAsset>("verify_closed_caption_asset_too_large");
1419 }
1420
1421
1422 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_language)
1423 {
1424         path dir = "build/test/verify_missing_subtitle_language";
1425         prepare_directory (dir);
1426         auto dcp = make_simple (dir, 1, 106);
1427
1428         string const xml =
1429                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1430                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
1431                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1432                 "<ContentTitleText>Content</ContentTitleText>"
1433                 "<AnnotationText>Annotation</AnnotationText>"
1434                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1435                 "<ReelNumber>1</ReelNumber>"
1436                 "<EditRate>24 1</EditRate>"
1437                 "<TimeCodeRate>24</TimeCodeRate>"
1438                 "<StartTime>00:00:00:00</StartTime>"
1439                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1440                 "<SubtitleList>"
1441                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1442                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1443                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1444                 "</Subtitle>"
1445                 "</Font>"
1446                 "</SubtitleList>"
1447                 "</SubtitleReel>";
1448
1449         dcp::File xml_file(dir / "subs.xml", "w");
1450         BOOST_REQUIRE (xml_file);
1451         xml_file.write(xml.c_str(), xml.size(), 1);
1452         xml_file.close();
1453         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1454         subs->write (dir / "subs.mxf");
1455
1456         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1457         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1458         dcp->write_xml();
1459
1460         check_verify_result (
1461                 { dir },
1462                 {
1463                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
1464                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1465                 });
1466 }
1467
1468
1469 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_languages)
1470 {
1471         path path ("build/test/verify_mismatched_subtitle_languages");
1472         auto constexpr reel_length = 192;
1473         auto dcp = make_simple (path, 2, reel_length);
1474         auto cpl = dcp->cpls()[0];
1475
1476         {
1477                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1478                 subs->set_language (dcp::LanguageTag("de-DE"));
1479                 subs->add (simple_subtitle());
1480                 add_font(subs);
1481                 subs->write (path / "subs1.mxf");
1482                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1483                 cpl->reels()[0]->add(reel_subs);
1484         }
1485
1486         {
1487                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1488                 subs->set_language (dcp::LanguageTag("en-US"));
1489                 subs->add (simple_subtitle());
1490                 add_font(subs);
1491                 subs->write (path / "subs2.mxf");
1492                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1493                 cpl->reels()[1]->add(reel_subs);
1494         }
1495
1496         dcp->write_xml();
1497
1498         check_verify_result (
1499                 { path },
1500                 {
1501                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1502                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") },
1503                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES }
1504                 });
1505 }
1506
1507
1508 BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
1509 {
1510         path path ("build/test/verify_multiple_closed_caption_languages_allowed");
1511         auto constexpr reel_length = 192;
1512         auto dcp = make_simple (path, 2, reel_length);
1513         auto cpl = dcp->cpls()[0];
1514
1515         {
1516                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1517                 ccaps->set_language (dcp::LanguageTag("de-DE"));
1518                 ccaps->add (simple_subtitle());
1519                 add_font(ccaps);
1520                 ccaps->write (path / "subs1.mxf");
1521                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1522                 cpl->reels()[0]->add(reel_ccaps);
1523         }
1524
1525         {
1526                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1527                 ccaps->set_language (dcp::LanguageTag("en-US"));
1528                 ccaps->add (simple_subtitle());
1529                 add_font(ccaps);
1530                 ccaps->write (path / "subs2.mxf");
1531                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1532                 cpl->reels()[1]->add(reel_ccaps);
1533         }
1534
1535         dcp->write_xml();
1536
1537         check_verify_result (
1538                 { path },
1539                 {
1540                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1541                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") }
1542                 });
1543 }
1544
1545
1546 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_start_time)
1547 {
1548         path dir = "build/test/verify_missing_subtitle_start_time";
1549         prepare_directory (dir);
1550         auto dcp = make_simple (dir, 1, 106);
1551
1552         string const xml =
1553                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1554                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
1555                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1556                 "<ContentTitleText>Content</ContentTitleText>"
1557                 "<AnnotationText>Annotation</AnnotationText>"
1558                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1559                 "<ReelNumber>1</ReelNumber>"
1560                 "<Language>de-DE</Language>"
1561                 "<EditRate>24 1</EditRate>"
1562                 "<TimeCodeRate>24</TimeCodeRate>"
1563                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1564                 "<SubtitleList>"
1565                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1566                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1567                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1568                 "</Subtitle>"
1569                 "</Font>"
1570                 "</SubtitleList>"
1571                 "</SubtitleReel>";
1572
1573         dcp::File xml_file(dir / "subs.xml", "w");
1574         BOOST_REQUIRE (xml_file);
1575         xml_file.write(xml.c_str(), xml.size(), 1);
1576         xml_file.close();
1577         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1578         subs->write (dir / "subs.mxf");
1579
1580         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1581         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1582         dcp->write_xml();
1583
1584         check_verify_result (
1585                 { dir },
1586                 {
1587                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1588                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1589                 });
1590 }
1591
1592
1593 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_start_time)
1594 {
1595         path dir = "build/test/verify_invalid_subtitle_start_time";
1596         prepare_directory (dir);
1597         auto dcp = make_simple (dir, 1, 106);
1598
1599         string const xml =
1600                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1601                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
1602                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1603                 "<ContentTitleText>Content</ContentTitleText>"
1604                 "<AnnotationText>Annotation</AnnotationText>"
1605                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1606                 "<ReelNumber>1</ReelNumber>"
1607                 "<Language>de-DE</Language>"
1608                 "<EditRate>24 1</EditRate>"
1609                 "<TimeCodeRate>24</TimeCodeRate>"
1610                 "<StartTime>00:00:02:00</StartTime>"
1611                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1612                 "<SubtitleList>"
1613                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1614                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1615                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1616                 "</Subtitle>"
1617                 "</Font>"
1618                 "</SubtitleList>"
1619                 "</SubtitleReel>";
1620
1621         dcp::File xml_file(dir / "subs.xml", "w");
1622         BOOST_REQUIRE (xml_file);
1623         xml_file.write(xml.c_str(), xml.size(), 1);
1624         xml_file.close();
1625         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1626         subs->write (dir / "subs.mxf");
1627
1628         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1629         dcp->cpls().front()->reels().front()->add(reel_subs);
1630         dcp->write_xml();
1631
1632         check_verify_result (
1633                 { dir },
1634                 {
1635                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1636                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1637                 });
1638 }
1639
1640
1641 class TestText
1642 {
1643 public:
1644         TestText (int in_, int out_, float v_position_ = 0, dcp::VAlign v_align_ = dcp::VAlign::CENTER, string text_ = "Hello")
1645                 : in(in_)
1646                 , out(out_)
1647                 , v_position(v_position_)
1648                 , v_align(v_align_)
1649                 , text(text_)
1650         {}
1651
1652         int in;
1653         int out;
1654         float v_position;
1655         dcp::VAlign v_align;
1656         string text;
1657 };
1658
1659
1660 template <class T>
1661 shared_ptr<dcp::CPL>
1662 dcp_with_text (path dir, vector<TestText> subs)
1663 {
1664         prepare_directory (dir);
1665         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1666         asset->set_start_time (dcp::Time());
1667         for (auto i: subs) {
1668                 add_test_subtitle (asset, i.in, i.out, i.v_position, i.v_align, i.text);
1669         }
1670         asset->set_language (dcp::LanguageTag("de-DE"));
1671         add_font(asset);
1672         asset->write (dir / "subs.mxf");
1673
1674         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1675         return write_dcp_with_single_asset (dir, reel_asset);
1676 }
1677
1678
1679 template <class T>
1680 shared_ptr<dcp::CPL>
1681 dcp_with_text_from_file (path dir, boost::filesystem::path subs_xml)
1682 {
1683         prepare_directory (dir);
1684         auto asset = make_shared<dcp::SMPTESubtitleAsset>(subs_xml);
1685         asset->set_start_time (dcp::Time());
1686         asset->set_language (dcp::LanguageTag("de-DE"));
1687
1688         auto subs_mxf = dir / "subs.mxf";
1689         asset->write (subs_mxf);
1690
1691         /* The call to write() puts the asset into the DCP correctly but it will have
1692          * XML re-written by our parser.  Overwrite the MXF using the given file's verbatim
1693          * contents.
1694          */
1695         ASDCP::TimedText::MXFWriter writer;
1696         ASDCP::WriterInfo writer_info;
1697         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
1698         unsigned int c;
1699         Kumu::hex2bin (asset->id().c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
1700         DCP_ASSERT (c == Kumu::UUID_Length);
1701         ASDCP::TimedText::TimedTextDescriptor descriptor;
1702         descriptor.ContainerDuration = asset->intrinsic_duration();
1703         Kumu::hex2bin (asset->xml_id()->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
1704         DCP_ASSERT (c == Kumu::UUID_Length);
1705         ASDCP::Result_t r = writer.OpenWrite (subs_mxf.string().c_str(), writer_info, descriptor, 16384);
1706         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1707         r = writer.WriteTimedTextResource (dcp::file_to_string(subs_xml));
1708         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1709         writer.Finalize ();
1710
1711         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1712         return write_dcp_with_single_asset (dir, reel_asset);
1713 }
1714
1715
1716 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
1717 {
1718         auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
1719         /* Just too early */
1720         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
1721         check_verify_result (
1722                 { dir },
1723                 {
1724                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1725                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1726                 });
1727
1728 }
1729
1730
1731 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time)
1732 {
1733         auto const dir = path("build/test/verify_valid_subtitle_first_text_time");
1734         /* Just late enough */
1735         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
1736         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1737 }
1738
1739
1740 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time_on_second_reel)
1741 {
1742         auto const dir = path("build/test/verify_valid_subtitle_first_text_time_on_second_reel");
1743         prepare_directory (dir);
1744
1745         auto asset1 = make_shared<dcp::SMPTESubtitleAsset>();
1746         asset1->set_start_time (dcp::Time());
1747         /* Just late enough */
1748         add_test_subtitle (asset1, 4 * 24, 5 * 24);
1749         asset1->set_language (dcp::LanguageTag("de-DE"));
1750         add_font(asset1);
1751         asset1->write (dir / "subs1.mxf");
1752         auto reel_asset1 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset1, dcp::Fraction(24, 1), 5 * 24, 0);
1753         auto reel1 = make_shared<dcp::Reel>();
1754         reel1->add (reel_asset1);
1755         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 5 * 24);
1756         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
1757         reel1->add (markers1);
1758
1759         auto asset2 = make_shared<dcp::SMPTESubtitleAsset>();
1760         asset2->set_start_time (dcp::Time());
1761         add_font(asset2);
1762         /* This would be too early on first reel but should be OK on the second */
1763         add_test_subtitle (asset2, 3, 4 * 24);
1764         asset2->set_language (dcp::LanguageTag("de-DE"));
1765         asset2->write (dir / "subs2.mxf");
1766         auto reel_asset2 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset2, dcp::Fraction(24, 1), 4 * 24, 0);
1767         auto reel2 = make_shared<dcp::Reel>();
1768         reel2->add (reel_asset2);
1769         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 4 * 24);
1770         markers2->set (dcp::Marker::LFOC, dcp::Time(4 * 24 - 1, 24, 24));
1771         reel2->add (markers2);
1772
1773         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1774         cpl->add (reel1);
1775         cpl->add (reel2);
1776         auto dcp = make_shared<dcp::DCP>(dir);
1777         dcp->add (cpl);
1778         dcp->set_annotation_text("hello");
1779         dcp->write_xml();
1780
1781         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1782 }
1783
1784
1785 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_spacing)
1786 {
1787         auto const dir = path("build/test/verify_invalid_subtitle_spacing");
1788         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1789                 dir,
1790                 {
1791                         { 4 * 24,     5 * 24 },
1792                         { 5 * 24 + 1, 6 * 24 },
1793                 });
1794         check_verify_result (
1795                 {dir},
1796                 {
1797                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING },
1798                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1799                 });
1800 }
1801
1802
1803 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_spacing)
1804 {
1805         auto const dir = path("build/test/verify_valid_subtitle_spacing");
1806         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1807                 dir,
1808                 {
1809                         { 4 * 24,      5 * 24 },
1810                         { 5 * 24 + 16, 8 * 24 },
1811                 });
1812         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1813 }
1814
1815
1816 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_duration)
1817 {
1818         auto const dir = path("build/test/verify_invalid_subtitle_duration");
1819         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
1820         check_verify_result (
1821                 {dir},
1822                 {
1823                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION },
1824                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1825                 });
1826 }
1827
1828
1829 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_duration)
1830 {
1831         auto const dir = path("build/test/verify_valid_subtitle_duration");
1832         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
1833         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1834 }
1835
1836
1837 BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
1838 {
1839         auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
1840         prepare_directory (dir);
1841         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1842         asset->set_start_time (dcp::Time());
1843         add_test_subtitle (asset, 0, 4 * 24);
1844         add_font(asset);
1845         asset->set_language (dcp::LanguageTag("de-DE"));
1846         asset->write (dir / "subs.mxf");
1847
1848         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
1849         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1850         check_verify_result (
1851                 {dir},
1852                 {
1853                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "72 96", boost::filesystem::canonical(asset->file().get()) },
1854                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1855                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
1856                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1857                 });
1858
1859 }
1860
1861
1862 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
1863 {
1864         auto const dir = path ("build/test/invalid_subtitle_line_count1");
1865         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1866                 dir,
1867                 {
1868                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1869                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1870                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1871                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1872                 });
1873         check_verify_result (
1874                 {dir},
1875                 {
1876                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1877                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1878                 });
1879 }
1880
1881
1882 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
1883 {
1884         auto const dir = path ("build/test/verify_valid_subtitle_line_count1");
1885         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1886                 dir,
1887                 {
1888                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1889                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1890                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1891                 });
1892         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1893 }
1894
1895
1896 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
1897 {
1898         auto const dir = path ("build/test/verify_invalid_subtitle_line_count2");
1899         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1900                 dir,
1901                 {
1902                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1903                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1904                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1905                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
1906                 });
1907         check_verify_result (
1908                 {dir},
1909                 {
1910                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1911                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1912                 });
1913 }
1914
1915
1916 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
1917 {
1918         auto const dir = path ("build/test/verify_valid_subtitle_line_count2");
1919         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1920                 dir,
1921                 {
1922                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1923                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1924                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1925                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
1926                 });
1927         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1928 }
1929
1930
1931 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
1932 {
1933         auto const dir = path ("build/test/verify_invalid_subtitle_line_length1");
1934         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1935                 dir,
1936                 {
1937                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123" }
1938                 });
1939         check_verify_result (
1940                 {dir},
1941                 {
1942                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH },
1943                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1944                 });
1945 }
1946
1947
1948 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
1949 {
1950         auto const dir = path ("build/test/verify_invalid_subtitle_line_length2");
1951         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1952                 dir,
1953                 {
1954                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
1955                 });
1956         check_verify_result (
1957                 {dir},
1958                 {
1959                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH },
1960                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1961                 });
1962 }
1963
1964
1965 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
1966 {
1967         auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
1968         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1969                 dir,
1970                 {
1971                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1972                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1973                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1974                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1975                 });
1976         check_verify_result (
1977                 {dir},
1978                 {
1979                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1980                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1981                 });
1982 }
1983
1984
1985 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
1986 {
1987         auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
1988         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1989                 dir,
1990                 {
1991                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1992                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1993                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1994                 });
1995         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1996 }
1997
1998
1999 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
2000 {
2001         auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
2002         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2003                 dir,
2004                 {
2005                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
2006                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
2007                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
2008                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
2009                 });
2010         check_verify_result (
2011                 {dir},
2012                 {
2013                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
2014                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2015                 });
2016 }
2017
2018
2019 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
2020 {
2021         auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
2022         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2023                 dir,
2024                 {
2025                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
2026                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
2027                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
2028                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
2029                 });
2030         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2031 }
2032
2033
2034 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_length)
2035 {
2036         auto const dir = path ("build/test/verify_valid_closed_caption_line_length");
2037         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2038                 dir,
2039                 {
2040                         { 96, 300, 0.0, dcp::VAlign::CENTER, "01234567890123456789012345678901" }
2041                 });
2042         check_verify_result (
2043                 {dir},
2044                 {
2045                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2046                 });
2047 }
2048
2049
2050 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
2051 {
2052         auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
2053         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2054                 dir,
2055                 {
2056                         { 96, 300, 0.0, dcp::VAlign::CENTER, "0123456789012345678901234567890123" }
2057                 });
2058         check_verify_result (
2059                 {dir},
2060                 {
2061                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH },
2062                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2063                 });
2064 }
2065
2066
2067 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign1)
2068 {
2069         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign1");
2070         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2071                 dir,
2072                 {
2073                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2074                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2075                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2076                 });
2077         check_verify_result (
2078                 {dir},
2079                 {
2080                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2081                 });
2082 }
2083
2084
2085 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign2)
2086 {
2087         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign2");
2088         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2089                 dir,
2090                 {
2091                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2092                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2093                         { 96, 300, 0.2, dcp::VAlign::CENTER, "not fine" },
2094                 });
2095         check_verify_result (
2096                 {dir},
2097                 {
2098                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN },
2099                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2100                 });
2101 }
2102
2103
2104 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering1)
2105 {
2106         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering1");
2107         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2108                 dir,
2109                 {
2110                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2111                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2112                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2113                 });
2114         check_verify_result (
2115                 {dir},
2116                 {
2117                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2118                 });
2119 }
2120
2121
2122 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering2)
2123 {
2124         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering2");
2125         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2126                 dir,
2127                 {
2128                         { 96, 300, 0.2, dcp::VAlign::BOTTOM, "This" },
2129                         { 96, 300, 0.1, dcp::VAlign::BOTTOM, "is" },
2130                         { 96, 300, 0.0, dcp::VAlign::BOTTOM, "also fine" },
2131                 });
2132         check_verify_result (
2133                 {dir},
2134                 {
2135                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2136                 });
2137 }
2138
2139
2140 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering3)
2141 {
2142         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering3");
2143         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering3.xml");
2144         check_verify_result (
2145                 {dir},
2146                 {
2147                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING },
2148                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2149                 });
2150 }
2151
2152
2153 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering4)
2154 {
2155         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering4");
2156         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering4.xml");
2157         check_verify_result (
2158                 {dir},
2159                 {
2160                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2161                 });
2162 }
2163
2164
2165
2166 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
2167 {
2168         path const dir("build/test/verify_invalid_sound_frame_rate");
2169         prepare_directory (dir);
2170
2171         auto picture = simple_picture (dir, "foo");
2172         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
2173         auto reel = make_shared<dcp::Reel>();
2174         reel->add (reel_picture);
2175         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000, boost::none);
2176         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
2177         reel->add (reel_sound);
2178         reel->add (simple_markers());
2179         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2180         cpl->add (reel);
2181         auto dcp = make_shared<dcp::DCP>(dir);
2182         dcp->add (cpl);
2183         dcp->set_annotation_text("hello");
2184         dcp->write_xml();
2185
2186         check_verify_result (
2187                 {dir},
2188                 {
2189                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE, string("96000"), canonical(dir / "audiofoo.mxf") },
2190                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2191                 });
2192 }
2193
2194
2195 BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
2196 {
2197         path const dir("build/test/verify_missing_cpl_annotation_text");
2198         auto dcp = make_simple (dir);
2199         dcp->write_xml();
2200
2201         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2202
2203         auto const cpl = dcp->cpls()[0];
2204
2205         {
2206                 BOOST_REQUIRE (cpl->file());
2207                 Editor e(cpl->file().get());
2208                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "");
2209         }
2210
2211         check_verify_result (
2212                 {dir},
2213                 {
2214                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2215                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2216                 });
2217 }
2218
2219
2220 BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
2221 {
2222         path const dir("build/test/verify_mismatched_cpl_annotation_text");
2223         auto dcp = make_simple (dir);
2224         dcp->write_xml();
2225
2226         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2227         auto const cpl = dcp->cpls()[0];
2228
2229         {
2230                 BOOST_REQUIRE (cpl->file());
2231                 Editor e(cpl->file().get());
2232                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "<AnnotationText>A Test DCP 1</AnnotationText>");
2233         }
2234
2235         check_verify_result (
2236                 {dir},
2237                 {
2238                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2239                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2240                 });
2241 }
2242
2243
2244 BOOST_AUTO_TEST_CASE (verify_mismatched_asset_duration)
2245 {
2246         path const dir("build/test/verify_mismatched_asset_duration");
2247         prepare_directory (dir);
2248         shared_ptr<dcp::DCP> dcp (new dcp::DCP(dir));
2249         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2250
2251         shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (dir, "", 24);
2252         shared_ptr<dcp::SoundAsset> ms = simple_sound (dir, "", dcp::MXFMetadata(), "en-US", 25);
2253
2254         auto reel = make_shared<dcp::Reel>(
2255                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2256                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2257                 );
2258
2259         reel->add (simple_markers());
2260         cpl->add (reel);
2261
2262         dcp->add (cpl);
2263         dcp->set_annotation_text("A Test DCP");
2264         dcp->write_xml();
2265
2266         check_verify_result (
2267                 {dir},
2268                 {
2269                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION },
2270                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), canonical(cpl->file().get()) }
2271                 });
2272 }
2273
2274
2275
2276 static
2277 shared_ptr<dcp::CPL>
2278 verify_subtitles_must_be_in_all_reels_check (path dir, bool add_to_reel1, bool add_to_reel2)
2279 {
2280         prepare_directory (dir);
2281         auto dcp = make_shared<dcp::DCP>(dir);
2282         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2283
2284         auto constexpr reel_length = 192;
2285
2286         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2287         subs->set_language (dcp::LanguageTag("de-DE"));
2288         subs->set_start_time (dcp::Time());
2289         subs->add (simple_subtitle());
2290         add_font(subs);
2291         subs->write (dir / "subs.mxf");
2292         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
2293
2294         auto reel1 = make_shared<dcp::Reel>(
2295                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "1", reel_length), 0),
2296                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "1", dcp::MXFMetadata(), "en-US", reel_length), 0)
2297                 );
2298
2299         if (add_to_reel1) {
2300                 reel1->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2301         }
2302
2303         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2304         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2305         reel1->add (markers1);
2306
2307         cpl->add (reel1);
2308
2309         auto reel2 = make_shared<dcp::Reel>(
2310                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "2", reel_length), 0),
2311                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "2", dcp::MXFMetadata(), "en-US", reel_length), 0)
2312                 );
2313
2314         if (add_to_reel2) {
2315                 reel2->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2316         }
2317
2318         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2319         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2320         reel2->add (markers2);
2321
2322         cpl->add (reel2);
2323
2324         dcp->add (cpl);
2325         dcp->set_annotation_text("A Test DCP");
2326         dcp->write_xml();
2327
2328         return cpl;
2329 }
2330
2331
2332 BOOST_AUTO_TEST_CASE (verify_missing_main_subtitle_from_some_reels)
2333 {
2334         {
2335                 path dir ("build/test/missing_main_subtitle_from_some_reels");
2336                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, false);
2337                 check_verify_result (
2338                         { dir },
2339                         {
2340                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS },
2341                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2342                         });
2343
2344         }
2345
2346         {
2347                 path dir ("build/test/verify_subtitles_must_be_in_all_reels2");
2348                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, true);
2349                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2350         }
2351
2352         {
2353                 path dir ("build/test/verify_subtitles_must_be_in_all_reels1");
2354                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, false, false);
2355                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2356         }
2357 }
2358
2359
2360 static
2361 shared_ptr<dcp::CPL>
2362 verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1, int caps_in_reel2)
2363 {
2364         prepare_directory (dir);
2365         auto dcp = make_shared<dcp::DCP>(dir);
2366         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2367
2368         auto constexpr reel_length = 192;
2369
2370         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2371         subs->set_language (dcp::LanguageTag("de-DE"));
2372         subs->set_start_time (dcp::Time());
2373         subs->add (simple_subtitle());
2374         add_font(subs);
2375         subs->write (dir / "subs.mxf");
2376
2377         auto reel1 = make_shared<dcp::Reel>(
2378                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "1", reel_length), 0),
2379                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "1", dcp::MXFMetadata(), "en-US", reel_length), 0)
2380                 );
2381
2382         for (int i = 0; i < caps_in_reel1; ++i) {
2383                 reel1->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2384         }
2385
2386         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2387         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2388         reel1->add (markers1);
2389
2390         cpl->add (reel1);
2391
2392         auto reel2 = make_shared<dcp::Reel>(
2393                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "2", reel_length), 0),
2394                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "2", dcp::MXFMetadata(), "en-US", reel_length), 0)
2395                 );
2396
2397         for (int i = 0; i < caps_in_reel2; ++i) {
2398                 reel2->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2399         }
2400
2401         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2402         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2403         reel2->add (markers2);
2404
2405         cpl->add (reel2);
2406
2407         dcp->add (cpl);
2408         dcp->set_annotation_text("A Test DCP");
2409         dcp->write_xml();
2410
2411         return cpl;
2412 }
2413
2414
2415 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_asset_counts)
2416 {
2417         {
2418                 path dir ("build/test/mismatched_closed_caption_asset_counts");
2419                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 3, 4);
2420                 check_verify_result (
2421                         {dir},
2422                         {
2423                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS },
2424                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2425                         });
2426         }
2427
2428         {
2429                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels2");
2430                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 4, 4);
2431                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2432         }
2433
2434         {
2435                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels3");
2436                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 0, 0);
2437                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2438         }
2439 }
2440
2441
2442 template <class T>
2443 void
2444 verify_text_entry_point_check (path dir, dcp::VerificationNote::Code code, boost::function<void (shared_ptr<T>)> adjust)
2445 {
2446         prepare_directory (dir);
2447         auto dcp = make_shared<dcp::DCP>(dir);
2448         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2449
2450         auto constexpr reel_length = 192;
2451
2452         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2453         subs->set_language (dcp::LanguageTag("de-DE"));
2454         subs->set_start_time (dcp::Time());
2455         subs->add (simple_subtitle());
2456         add_font(subs);
2457         subs->write (dir / "subs.mxf");
2458         auto reel_text = make_shared<T>(subs, dcp::Fraction(24, 1), reel_length, 0);
2459         adjust (reel_text);
2460
2461         auto reel = make_shared<dcp::Reel>(
2462                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2463                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2464                 );
2465
2466         reel->add (reel_text);
2467
2468         reel->add (simple_markers(reel_length));
2469
2470         cpl->add (reel);
2471
2472         dcp->add (cpl);
2473         dcp->set_annotation_text("A Test DCP");
2474         dcp->write_xml();
2475
2476         check_verify_result (
2477                 {dir},
2478                 {
2479                         { dcp::VerificationNote::Type::BV21_ERROR, code, subs->id() },
2480                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2481                 });
2482 }
2483
2484
2485 BOOST_AUTO_TEST_CASE (verify_text_entry_point)
2486 {
2487         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2488                 "build/test/verify_subtitle_entry_point_must_be_present",
2489                 dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT,
2490                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2491                         asset->unset_entry_point ();
2492                         }
2493                 );
2494
2495         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2496                 "build/test/verify_subtitle_entry_point_must_be_zero",
2497                 dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT,
2498                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2499                         asset->set_entry_point (4);
2500                         }
2501                 );
2502
2503         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2504                 "build/test/verify_closed_caption_entry_point_must_be_present",
2505                 dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
2506                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2507                         asset->unset_entry_point ();
2508                         }
2509                 );
2510
2511         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2512                 "build/test/verify_closed_caption_entry_point_must_be_zero",
2513                 dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
2514                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2515                         asset->set_entry_point (9);
2516                         }
2517                 );
2518 }
2519
2520
2521 BOOST_AUTO_TEST_CASE (verify_missing_hash)
2522 {
2523         RNGFixer fix;
2524
2525         path const dir("build/test/verify_missing_hash");
2526         auto dcp = make_simple (dir);
2527         dcp->write_xml();
2528
2529         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2530         auto const cpl = dcp->cpls()[0];
2531         BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
2532         BOOST_REQUIRE (cpl->reels()[0]->main_picture());
2533         auto asset_id = cpl->reels()[0]->main_picture()->id();
2534
2535         {
2536                 BOOST_REQUIRE (cpl->file());
2537                 Editor e(cpl->file().get());
2538                 e.delete_first_line_containing("<Hash>");
2539         }
2540
2541         check_verify_result (
2542                 {dir},
2543                 {
2544                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2545                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
2546                 });
2547 }
2548
2549
2550 static
2551 void
2552 verify_markers_test (
2553         path dir,
2554         vector<pair<dcp::Marker, dcp::Time>> markers,
2555         vector<dcp::VerificationNote> test_notes
2556         )
2557 {
2558         auto dcp = make_simple (dir);
2559         dcp->cpls()[0]->set_content_kind (dcp::ContentKind::FEATURE);
2560         auto markers_asset = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 24);
2561         for (auto const& i: markers) {
2562                 markers_asset->set (i.first, i.second);
2563         }
2564         dcp->cpls()[0]->reels()[0]->add(markers_asset);
2565         dcp->write_xml();
2566
2567         check_verify_result ({dir}, test_notes);
2568 }
2569
2570
2571 BOOST_AUTO_TEST_CASE (verify_markers)
2572 {
2573         verify_markers_test (
2574                 "build/test/verify_markers_all_correct",
2575                 {
2576                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2577                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2578                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2579                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2580                 },
2581                 {}
2582                 );
2583
2584         verify_markers_test (
2585                 "build/test/verify_markers_missing_ffec",
2586                 {
2587                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2588                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2589                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2590                 },
2591                 {
2592                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE }
2593                 });
2594
2595         verify_markers_test (
2596                 "build/test/verify_markers_missing_ffmc",
2597                 {
2598                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2599                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2600                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2601                 },
2602                 {
2603                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
2604                 });
2605
2606         verify_markers_test (
2607                 "build/test/verify_markers_missing_ffoc",
2608                 {
2609                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2610                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2611                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2612                 },
2613                 {
2614                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC}
2615                 });
2616
2617         verify_markers_test (
2618                 "build/test/verify_markers_missing_lfoc",
2619                 {
2620                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2621                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2622                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) }
2623                 },
2624                 {
2625                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC }
2626                 });
2627
2628         verify_markers_test (
2629                 "build/test/verify_markers_incorrect_ffoc",
2630                 {
2631                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2632                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2633                         { dcp::Marker::FFOC, dcp::Time(3, 24, 24) },
2634                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2635                 },
2636                 {
2637                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_FFOC, string("3") }
2638                 });
2639
2640         verify_markers_test (
2641                 "build/test/verify_markers_incorrect_lfoc",
2642                 {
2643                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2644                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2645                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2646                         { dcp::Marker::LFOC, dcp::Time(18, 24, 24) }
2647                 },
2648                 {
2649                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_LFOC, string("18") }
2650                 });
2651 }
2652
2653
2654 BOOST_AUTO_TEST_CASE (verify_missing_cpl_metadata_version_number)
2655 {
2656         path dir = "build/test/verify_missing_cpl_metadata_version_number";
2657         prepare_directory (dir);
2658         auto dcp = make_simple (dir);
2659         auto cpl = dcp->cpls()[0];
2660         cpl->unset_version_number();
2661         dcp->write_xml();
2662
2663         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get() }});
2664 }
2665
2666
2667 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
2668 {
2669         path dir = "build/test/verify_missing_extension_metadata1";
2670         auto dcp = make_simple (dir);
2671         dcp->write_xml();
2672
2673         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2674         auto cpl = dcp->cpls()[0];
2675
2676         {
2677                 Editor e (cpl->file().get());
2678                 e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
2679         }
2680
2681         check_verify_result (
2682                 {dir},
2683                 {
2684                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2685                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2686                 });
2687 }
2688
2689
2690 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
2691 {
2692         path dir = "build/test/verify_missing_extension_metadata2";
2693         auto dcp = make_simple (dir);
2694         dcp->write_xml();
2695
2696         auto cpl = dcp->cpls()[0];
2697
2698         {
2699                 Editor e (cpl->file().get());
2700                 e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
2701         }
2702
2703         check_verify_result (
2704                 {dir},
2705                 {
2706                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2707                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2708                 });
2709 }
2710
2711
2712 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
2713 {
2714         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata3";
2715         auto dcp = make_simple (dir);
2716         dcp->write_xml();
2717
2718         auto const cpl = dcp->cpls()[0];
2719
2720         {
2721                 Editor e (cpl->file().get());
2722                 e.replace ("<meta:Name>A", "<meta:NameX>A");
2723                 e.replace ("n</meta:Name>", "n</meta:NameX>");
2724         }
2725
2726         check_verify_result (
2727                 {dir},
2728                 {
2729                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:NameX'"), cpl->file().get(), 70 },
2730                         { 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 },
2731                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2732                 });
2733 }
2734
2735
2736 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
2737 {
2738         path dir = "build/test/verify_invalid_extension_metadata1";
2739         auto dcp = make_simple (dir);
2740         dcp->write_xml();
2741
2742         auto cpl = dcp->cpls()[0];
2743
2744         {
2745                 Editor e (cpl->file().get());
2746                 e.replace ("Application", "Fred");
2747         }
2748
2749         check_verify_result (
2750                 {dir},
2751                 {
2752                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2753                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
2754                 });
2755 }
2756
2757
2758 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
2759 {
2760         path dir = "build/test/verify_invalid_extension_metadata2";
2761         auto dcp = make_simple (dir);
2762         dcp->write_xml();
2763
2764         auto cpl = dcp->cpls()[0];
2765
2766         {
2767                 Editor e (cpl->file().get());
2768                 e.replace ("DCP Constraints Profile", "Fred");
2769         }
2770
2771         check_verify_result (
2772                 {dir},
2773                 {
2774                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2775                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
2776                 });
2777 }
2778
2779
2780 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
2781 {
2782         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata6";
2783         auto dcp = make_simple (dir);
2784         dcp->write_xml();
2785
2786         auto const cpl = dcp->cpls()[0];
2787
2788         {
2789                 Editor e (cpl->file().get());
2790                 e.replace ("<meta:Value>", "<meta:ValueX>");
2791                 e.replace ("</meta:Value>", "</meta:ValueX>");
2792         }
2793
2794         check_verify_result (
2795                 {dir},
2796                 {
2797                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 74 },
2798                         { 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 },
2799                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2800                 });
2801 }
2802
2803
2804 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
2805 {
2806         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata7";
2807         auto dcp = make_simple (dir);
2808         dcp->write_xml();
2809
2810         auto const cpl = dcp->cpls()[0];
2811
2812         {
2813                 Editor e (cpl->file().get());
2814                 e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
2815         }
2816
2817         check_verify_result (
2818                 {dir},
2819                 {
2820                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2821                         { 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() },
2822                 });
2823 }
2824
2825
2826 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
2827 {
2828         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata8";
2829         auto dcp = make_simple (dir);
2830         dcp->write_xml();
2831
2832         auto const cpl = dcp->cpls()[0];
2833
2834         {
2835                 Editor e (cpl->file().get());
2836                 e.replace ("<meta:Property>", "<meta:PropertyX>");
2837                 e.replace ("</meta:Property>", "</meta:PropertyX>");
2838         }
2839
2840         check_verify_result (
2841                 {dir},
2842                 {
2843                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 72 },
2844                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 76 },
2845                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2846                 });
2847 }
2848
2849
2850 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
2851 {
2852         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata9";
2853         auto dcp = make_simple (dir);
2854         dcp->write_xml();
2855
2856         auto const cpl = dcp->cpls()[0];
2857
2858         {
2859                 Editor e (cpl->file().get());
2860                 e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
2861                 e.replace ("</meta:PropertyList>", "</meta:PropertyListX>");
2862         }
2863
2864         check_verify_result (
2865                 {dir},
2866                 {
2867                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 71 },
2868                         { 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 },
2869                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2870                 });
2871 }
2872
2873
2874
2875 BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
2876 {
2877         path dir = "build/test/verify_unsigned_cpl_with_encrypted_content";
2878         prepare_directory (dir);
2879         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2880                 copy_file (i.path(), dir / i.path().filename());
2881         }
2882
2883         path const pkl = dir / ( "pkl_" + encryption_test_pkl_id() + ".xml" );
2884         path const cpl = dir / ( "cpl_" + encryption_test_cpl_id() + ".xml");
2885
2886         {
2887                 Editor e (cpl);
2888                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2889         }
2890
2891         check_verify_result (
2892                 {dir},
2893                 {
2894                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id(), canonical(cpl) },
2895                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id(), canonical(pkl), },
2896                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2897                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2898                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2899                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2900                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id(), canonical(cpl) },
2901                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, encryption_test_cpl_id(), canonical(cpl) }
2902                 });
2903 }
2904
2905
2906 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_encrypted_content)
2907 {
2908         path dir = "build/test/unsigned_pkl_with_encrypted_content";
2909         prepare_directory (dir);
2910         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2911                 copy_file (i.path(), dir / i.path().filename());
2912         }
2913
2914         path const cpl = dir / ("cpl_" + encryption_test_cpl_id() + ".xml");
2915         path const pkl = dir / ("pkl_" + encryption_test_pkl_id() + ".xml");
2916         {
2917                 Editor e (pkl);
2918                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2919         }
2920
2921         check_verify_result (
2922                 {dir},
2923                 {
2924                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id(), canonical(pkl) },
2925                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2926                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2927                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2928                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2929                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id(), canonical(cpl) },
2930                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, encryption_test_pkl_id(), canonical(pkl) },
2931                 });
2932 }
2933
2934
2935 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_unencrypted_content)
2936 {
2937         path dir = "build/test/verify_unsigned_pkl_with_unencrypted_content";
2938         prepare_directory (dir);
2939         for (auto i: directory_iterator("test/ref/DCP/dcp_test1")) {
2940                 copy_file (i.path(), dir / i.path().filename());
2941         }
2942
2943         {
2944                 Editor e (dir / dcp_test1_pkl());
2945                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2946         }
2947
2948         check_verify_result ({dir}, {});
2949 }
2950
2951
2952 BOOST_AUTO_TEST_CASE (verify_partially_encrypted)
2953 {
2954         path dir ("build/test/verify_must_not_be_partially_encrypted");
2955         prepare_directory (dir);
2956
2957         dcp::DCP d (dir);
2958
2959         auto signer = make_shared<dcp::CertificateChain>();
2960         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem")));
2961         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem")));
2962         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem")));
2963         signer->set_key (dcp::file_to_string("test/ref/crypt/leaf.key"));
2964
2965         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2966
2967         dcp::Key key;
2968
2969         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
2970         mp->set_key (key);
2971
2972         auto writer = mp->start_write(dir / "video.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
2973         dcp::ArrayData j2c ("test/data/flat_red.j2c");
2974         for (int i = 0; i < 24; ++i) {
2975                 writer->write (j2c.data(), j2c.size());
2976         }
2977         writer->finalize ();
2978
2979         auto ms = simple_sound (dir, "", dcp::MXFMetadata(), "de-DE");
2980
2981         auto reel = make_shared<dcp::Reel>(
2982                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2983                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2984                 );
2985
2986         reel->add (simple_markers());
2987
2988         cpl->add (reel);
2989
2990         cpl->set_content_version (
2991                 {"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"}
2992                 );
2993         cpl->set_annotation_text ("A Test DCP");
2994         cpl->set_issuer ("OpenDCP 0.0.25");
2995         cpl->set_creator ("OpenDCP 0.0.25");
2996         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
2997         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,C,R,LFE,-,-"));
2998         cpl->set_main_sound_sample_rate (48000);
2999         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
3000         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
3001         cpl->set_version_number (1);
3002
3003         d.add (cpl);
3004
3005         d.set_issuer("OpenDCP 0.0.25");
3006         d.set_creator("OpenDCP 0.0.25");
3007         d.set_issue_date("2012-07-17T04:45:18+00:00");
3008         d.set_annotation_text("A Test DCP");
3009         d.write_xml(signer);
3010
3011         check_verify_result (
3012                 {dir},
3013                 {
3014                         {dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED},
3015                 });
3016 }
3017
3018
3019 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_2k)
3020 {
3021         vector<dcp::VerificationNote> notes;
3022         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"));
3023         auto reader = picture.start_read ();
3024         auto frame = reader->get_frame (0);
3025         verify_j2k(frame, 0, 24, notes);
3026         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3027 }
3028
3029
3030 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_4k)
3031 {
3032         vector<dcp::VerificationNote> notes;
3033         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "sul", "TLR"));
3034         auto reader = picture.start_read ();
3035         auto frame = reader->get_frame (0);
3036         verify_j2k(frame, 0, 24, notes);
3037         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3038 }
3039
3040
3041 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_libdcp)
3042 {
3043         boost::filesystem::path dir = "build/test/verify_jpeg2000_codestream_libdcp";
3044         prepare_directory (dir);
3045         auto dcp = make_simple (dir);
3046         dcp->write_xml ();
3047         vector<dcp::VerificationNote> notes;
3048         dcp::MonoPictureAsset picture (find_file(dir, "video"));
3049         auto reader = picture.start_read ();
3050         auto frame = reader->get_frame (0);
3051         verify_j2k(frame, 0, 24, notes);
3052         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3053 }
3054
3055
3056 /** Check that ResourceID and the XML ID being different is spotted */
3057 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_resource_id)
3058 {
3059         boost::filesystem::path const dir = "build/test/verify_mismatched_subtitle_resource_id";
3060         prepare_directory (dir);
3061
3062         ASDCP::WriterInfo writer_info;
3063         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3064
3065         unsigned int c;
3066         auto mxf_id = dcp::make_uuid ();
3067         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3068         BOOST_REQUIRE (c == Kumu::UUID_Length);
3069
3070         auto resource_id = dcp::make_uuid ();
3071         ASDCP::TimedText::TimedTextDescriptor descriptor;
3072         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3073         DCP_ASSERT (c == Kumu::UUID_Length);
3074
3075         auto xml_id = dcp::make_uuid ();
3076         ASDCP::TimedText::MXFWriter writer;
3077         auto subs_mxf = dir / "subs.mxf";
3078         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3079         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3080         writer.WriteTimedTextResource (dcp::String::compose(
3081                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3082                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
3083                 "<Id>urn:uuid:%1</Id>"
3084                 "<ContentTitleText>Content</ContentTitleText>"
3085                 "<AnnotationText>Annotation</AnnotationText>"
3086                 "<IssueDate>2018-10-02T12:25:14</IssueDate>"
3087                 "<ReelNumber>1</ReelNumber>"
3088                 "<Language>en-US</Language>"
3089                 "<EditRate>25 1</EditRate>"
3090                 "<TimeCodeRate>25</TimeCodeRate>"
3091                 "<StartTime>00:00:00:00</StartTime>"
3092                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
3093                 "<SubtitleList>"
3094                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3095                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3096                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3097                 "</Subtitle>"
3098                 "</Font>"
3099                 "</SubtitleList>"
3100                 "</SubtitleReel>",
3101                 xml_id).c_str());
3102
3103         writer.Finalize();
3104
3105         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3106         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3107
3108         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3109
3110         check_verify_result (
3111                 { dir },
3112                 {
3113                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3114                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID },
3115                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3116                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3117                 });
3118 }
3119
3120
3121 /** Check that ResourceID and the MXF ID being the same is spotted */
3122 BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
3123 {
3124         boost::filesystem::path const dir = "build/test/verify_incorrect_timed_text_id";
3125         prepare_directory (dir);
3126
3127         ASDCP::WriterInfo writer_info;
3128         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3129
3130         unsigned int c;
3131         auto mxf_id = dcp::make_uuid ();
3132         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3133         BOOST_REQUIRE (c == Kumu::UUID_Length);
3134
3135         auto resource_id = mxf_id;
3136         ASDCP::TimedText::TimedTextDescriptor descriptor;
3137         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3138         DCP_ASSERT (c == Kumu::UUID_Length);
3139
3140         auto xml_id = resource_id;
3141         ASDCP::TimedText::MXFWriter writer;
3142         auto subs_mxf = dir / "subs.mxf";
3143         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3144         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3145         writer.WriteTimedTextResource (dcp::String::compose(
3146                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3147                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
3148                 "<Id>urn:uuid:%1</Id>"
3149                 "<ContentTitleText>Content</ContentTitleText>"
3150                 "<AnnotationText>Annotation</AnnotationText>"
3151                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3152                 "<ReelNumber>1</ReelNumber>"
3153                 "<Language>en-US</Language>"
3154                 "<EditRate>25 1</EditRate>"
3155                 "<TimeCodeRate>25</TimeCodeRate>"
3156                 "<StartTime>00:00:00:00</StartTime>"
3157                 "<LoadFont ID=\"font\">urn:uuid:0ce6e0ba-58b9-4344-8929-4d9c959c2d55</LoadFont>"
3158                 "<SubtitleList>"
3159                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3160                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3161                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3162                 "</Subtitle>"
3163                 "</Font>"
3164                 "</SubtitleList>"
3165                 "</SubtitleReel>",
3166                 xml_id).c_str());
3167
3168         writer.Finalize();
3169
3170         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3171         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3172
3173         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3174
3175         check_verify_result (
3176                 { dir },
3177                 {
3178                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3179                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID },
3180                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3181                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
3182                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, string{"2018-10-02T12:25:14+02:00"} }
3183                 });
3184 }
3185
3186
3187 /** Check a DCP with a 3D asset marked as 2D */
3188 BOOST_AUTO_TEST_CASE (verify_threed_marked_as_twod)
3189 {
3190         check_verify_result (
3191                 { private_test / "data" / "xm" },
3192                 {
3193                         {
3194                                 dcp::VerificationNote::Type::WARNING,
3195                                 dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, boost::filesystem::canonical(find_file(private_test / "data" / "xm", "j2c"))
3196                         },
3197                         {
3198                                 dcp::VerificationNote::Type::BV21_ERROR,
3199                                 dcp::VerificationNote::Code::INVALID_STANDARD
3200                         },
3201                 });
3202
3203 }
3204
3205
3206 BOOST_AUTO_TEST_CASE (verify_unexpected_things_in_main_markers)
3207 {
3208         path dir = "build/test/verify_unexpected_things_in_main_markers";
3209         prepare_directory (dir);
3210         auto dcp = make_simple (dir, 1, 24);
3211         dcp->write_xml();
3212
3213         {
3214                 Editor e (find_cpl(dir));
3215                 e.insert(
3216                         "          <IntrinsicDuration>24</IntrinsicDuration>",
3217                         "<EntryPoint>0</EntryPoint><Duration>24</Duration>"
3218                         );
3219         }
3220
3221         dcp::CPL cpl (find_cpl(dir));
3222
3223         check_verify_result (
3224                 { dir },
3225                 {
3226                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3227                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_ENTRY_POINT },
3228                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_DURATION },
3229                 });
3230 }
3231
3232
3233 BOOST_AUTO_TEST_CASE(verify_invalid_content_kind)
3234 {
3235         path dir = "build/test/verify_invalid_content_kind";
3236         prepare_directory (dir);
3237         auto dcp = make_simple (dir, 1, 24);
3238         dcp->write_xml();
3239
3240         {
3241                 Editor e(find_cpl(dir));
3242                 e.replace("trailer", "trip");
3243         }
3244
3245         dcp::CPL cpl (find_cpl(dir));
3246
3247         check_verify_result (
3248                 { dir },
3249                 {
3250                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3251                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_CONTENT_KIND, string("trip") }
3252                 });
3253
3254 }
3255
3256
3257 BOOST_AUTO_TEST_CASE(verify_valid_content_kind)
3258 {
3259         path dir = "build/test/verify_valid_content_kind";
3260         prepare_directory (dir);
3261         auto dcp = make_simple (dir, 1, 24);
3262         dcp->write_xml();
3263
3264         {
3265                 Editor e(find_cpl(dir));
3266                 e.replace("<ContentKind>trailer</ContentKind>", "<ContentKind scope=\"http://bobs.contents/\">trip</ContentKind>");
3267         }
3268
3269         dcp::CPL cpl (find_cpl(dir));
3270
3271         check_verify_result (
3272                 { dir },
3273                 {
3274                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3275                 });
3276
3277 }
3278
3279
3280 BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_1)
3281 {
3282         path dir = "build/test/verify_invalid_main_picture_active_area_1";
3283         prepare_directory(dir);
3284         auto dcp = make_simple(dir, 1, 24);
3285         dcp->write_xml();
3286
3287         auto constexpr area = "<meta:MainPictureActiveArea>";
3288
3289         {
3290                 Editor e(find_cpl(dir));
3291                 e.delete_lines_after(area, 2);
3292                 e.insert(area, "<meta:Height>4080</meta:Height>");
3293                 e.insert(area, "<meta:Width>1997</meta:Width>");
3294         }
3295
3296         dcp::PKL pkl(find_pkl(dir));
3297         dcp::CPL cpl(find_cpl(dir));
3298
3299         check_verify_result(
3300                 { dir },
3301                 {
3302                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3303                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "width 1997 is not a multiple of 2", canonical(find_cpl(dir)) },
3304                         { 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)) },
3305                 });
3306 }
3307
3308
3309 BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_2)
3310 {
3311         path dir = "build/test/verify_invalid_main_picture_active_area_2";
3312         prepare_directory(dir);
3313         auto dcp = make_simple(dir, 1, 24);
3314         dcp->write_xml();
3315
3316         auto constexpr area = "<meta:MainPictureActiveArea>";
3317
3318         {
3319                 Editor e(find_cpl(dir));
3320                 e.delete_lines_after(area, 2);
3321                 e.insert(area, "<meta:Height>5125</meta:Height>");
3322                 e.insert(area, "<meta:Width>9900</meta:Width>");
3323         }
3324
3325         dcp::PKL pkl(find_pkl(dir));
3326         dcp::CPL cpl(find_cpl(dir));
3327
3328         check_verify_result(
3329                 { dir },
3330                 {
3331                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3332                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 5125 is not a multiple of 2", canonical(find_cpl(dir)) },
3333                         { 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)) },
3334                         { 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)) },
3335                 });
3336 }
3337
3338
3339 BOOST_AUTO_TEST_CASE(verify_duplicate_pkl_asset_ids)
3340 {
3341         RNGFixer rg;
3342
3343         path dir = "build/test/verify_duplicate_pkl_asset_ids";
3344         prepare_directory(dir);
3345         auto dcp = make_simple(dir, 1, 24);
3346         dcp->write_xml();
3347
3348         {
3349                 Editor e(find_pkl(dir));
3350                 e.replace("urn:uuid:5407b210-4441-4e97-8b16-8bdc7c12da54", "urn:uuid:6affb8ee-0020-4dff-a53c-17652f6358ab");
3351         }
3352
3353         dcp::PKL pkl(find_pkl(dir));
3354
3355         check_verify_result(
3356                 { dir },
3357                 {
3358                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl.id(), canonical(find_pkl(dir)) },
3359                 });
3360 }
3361
3362
3363 BOOST_AUTO_TEST_CASE(verify_duplicate_assetmap_asset_ids)
3364 {
3365         RNGFixer rg;
3366
3367         path dir = "build/test/verify_duplicate_assetmap_asset_ids";
3368         prepare_directory(dir);
3369         auto dcp = make_simple(dir, 1, 24);
3370         dcp->write_xml();
3371
3372         {
3373                 Editor e(find_asset_map(dir));
3374                 e.replace("urn:uuid:5407b210-4441-4e97-8b16-8bdc7c12da54", "urn:uuid:97f0f352-5b77-48ee-a558-9df37717f4fa");
3375         }
3376
3377         dcp::PKL pkl(find_pkl(dir));
3378         dcp::AssetMap asset_map(find_asset_map(dir));
3379
3380         check_verify_result(
3381                 { dir },
3382                 {
3383                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map.id(), canonical(find_asset_map(dir)) },
3384                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, string("5407b210-4441-4e97-8b16-8bdc7c12da54") },
3385                 });
3386 }
3387
3388
3389 BOOST_AUTO_TEST_CASE(verify_mismatched_sound_channel_counts)
3390 {
3391         boost::filesystem::path const path = "build/test/verify_mismatched_sound_channel_counts";
3392
3393         dcp::MXFMetadata mxf_meta;
3394         mxf_meta.company_name = "OpenDCP";
3395         mxf_meta.product_name = "OpenDCP";
3396         mxf_meta.product_version = "0.0.25";
3397
3398         auto constexpr sample_rate = 48000;
3399         auto constexpr frames = 240;
3400
3401         boost::filesystem::remove_all(path);
3402         boost::filesystem::create_directories(path);
3403         auto dcp = make_shared<dcp::DCP>(path);
3404         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
3405         cpl->set_annotation_text("hello");
3406         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,R"));
3407         cpl->set_main_sound_sample_rate(sample_rate);
3408         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
3409         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
3410         cpl->set_version_number(1);
3411
3412         {
3413
3414                 /* Reel with 2 channels of audio */
3415
3416                 auto mp = simple_picture(path, "1", frames, {});
3417                 auto ms = simple_sound(path, "1", mxf_meta, "en-US", frames, sample_rate, {}, 2);
3418
3419                 auto reel = make_shared<dcp::Reel>(
3420                         std::make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3421                         std::make_shared<dcp::ReelSoundAsset>(ms, 0)
3422                         );
3423
3424                 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
3425                 markers->set(dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
3426                 reel->add(markers);
3427
3428                 cpl->add(reel);
3429         }
3430
3431         {
3432                 /* Reel with 6 channels of audio */
3433
3434                 auto mp = simple_picture(path, "2", frames, {});
3435                 auto ms = simple_sound(path, "2", mxf_meta, "en-US", frames, sample_rate, {}, 6);
3436
3437                 auto reel = make_shared<dcp::Reel>(
3438                         std::make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3439                         std::make_shared<dcp::ReelSoundAsset>(ms, 0)
3440                         );
3441
3442                 auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
3443                 markers->set(dcp::Marker::LFOC, dcp::Time(0, 0, 0, frames - 1, 24));
3444                 reel->add(markers);
3445
3446                 cpl->add(reel);
3447         }
3448
3449         dcp->add(cpl);
3450         dcp->set_annotation_text("hello");
3451         dcp->write_xml();
3452
3453         check_verify_result(
3454                 { path },
3455                 {
3456                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_SOUND_CHANNEL_COUNTS, canonical(find_file(path, "audio2")) },
3457                 });
3458 }
3459
3460
3461 BOOST_AUTO_TEST_CASE(verify_invalid_main_sound_configuration)
3462 {
3463         boost::filesystem::path const path = "build/test/verify_invalid_main_sound_configuration";
3464
3465         dcp::MXFMetadata mxf_meta;
3466         mxf_meta.company_name = "OpenDCP";
3467         mxf_meta.product_name = "OpenDCP";
3468         mxf_meta.product_version = "0.0.25";
3469
3470         auto constexpr sample_rate = 48000;
3471         auto constexpr frames = 240;
3472
3473         boost::filesystem::remove_all(path);
3474         boost::filesystem::create_directories(path);
3475         auto dcp = make_shared<dcp::DCP>(path);
3476         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
3477         cpl->set_annotation_text("hello");
3478         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,R,C,LFE,Ls,Rs"));
3479         cpl->set_main_sound_sample_rate(sample_rate);
3480         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
3481         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
3482         cpl->set_version_number(1);
3483
3484         auto mp = simple_picture(path, "1", frames, {});
3485         auto ms = simple_sound(path, "1", mxf_meta, "en-US", frames, sample_rate, {}, 2);
3486
3487         auto reel = make_shared<dcp::Reel>(
3488                 std::make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3489                 std::make_shared<dcp::ReelSoundAsset>(ms, 0)
3490                 );
3491
3492         auto markers = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), frames);
3493         markers->set(dcp::Marker::FFOC, dcp::Time(0, 0, 0, 1, 24));
3494         markers->set(dcp::Marker::LFOC, dcp::Time(0, 0, 9, 23, 24));
3495         reel->add(markers);
3496
3497         cpl->add(reel);
3498
3499         dcp->add(cpl);
3500         dcp->set_annotation_text("hello");
3501         dcp->write_xml();
3502
3503         check_verify_result(
3504                 { path },
3505                 {
3506                         { 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)) },
3507                 });
3508 }
3509
3510
3511 BOOST_AUTO_TEST_CASE(verify_invalid_tile_part_size)
3512 {
3513         boost::filesystem::path const path = "build/test/verify_invalid_tile_part_size";
3514         auto constexpr video_frames = 24;
3515         auto constexpr sample_rate = 48000;
3516
3517         boost::filesystem::remove_all(path);
3518         boost::filesystem::create_directories(path);
3519
3520         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(24, 1), dcp::Standard::SMPTE);
3521         auto picture_writer = mp->start_write(path / "video.mxf", dcp::PictureAsset::Behaviour::MAKE_NEW);
3522
3523         dcp::Size const size(1998, 1080);
3524         auto image = make_shared<dcp::OpenJPEGImage>(size);
3525         boost::random::mt19937 rng(1);
3526         boost::random::uniform_int_distribution<> dist(0, 4095);
3527         for (int c = 0; c < 3; ++c) {
3528                 for (int p = 0; p < (1998 * 1080); ++p) {
3529                         image->data(c)[p] = dist(rng);
3530                 }
3531         }
3532         auto j2c = dcp::compress_j2k(image, 750000000, video_frames, false, false);
3533         for (int i = 0; i < 24; ++i) {
3534                 picture_writer->write(j2c.data(), j2c.size());
3535         }
3536         picture_writer->finalize();
3537
3538         auto dcp = make_shared<dcp::DCP>(path);
3539         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
3540         cpl->set_content_version(
3541                 dcp::ContentVersion("urn:uuid:75ac29aa-42ac-1234-ecae-49251abefd11", "content-version-label-text")
3542                 );
3543         cpl->set_main_sound_configuration(dcp::MainSoundConfiguration("51/L,R,C,LFE,Ls,Rs"));
3544         cpl->set_main_sound_sample_rate(sample_rate);
3545         cpl->set_main_picture_stored_area(dcp::Size(1998, 1080));
3546         cpl->set_main_picture_active_area(dcp::Size(1998, 1080));
3547         cpl->set_version_number(1);
3548
3549         auto ms = simple_sound(path, "", dcp::MXFMetadata(), "en-US", video_frames, sample_rate, {});
3550
3551         auto reel = make_shared<dcp::Reel>(
3552                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3553                 make_shared<dcp::ReelSoundAsset>(ms, 0)
3554                 );
3555
3556         cpl->add(reel);
3557         dcp->add(cpl);
3558         dcp->set_annotation_text("A Test DCP");
3559         dcp->write_xml();
3560
3561         check_verify_result(
3562                 { path },
3563                 {
3564                         dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_JPEG2000_TILE_PART_SIZE).set_frame(0).set_component(0).set_size(1321721),
3565                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_SIZE_IN_BYTES, canonical(path / "video.mxf") },
3566                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
3567                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
3568                 });
3569 }
3570
3571
3572 BOOST_AUTO_TEST_CASE(verify_too_many_subtitle_namespaces)
3573 {
3574         boost::filesystem::path const dir = "test/ref/DCP/subtitle_namespace_test";
3575         check_verify_result(
3576                 { dir },
3577                 {
3578                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
3579                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
3580                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3581                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(find_file(dir, "sub_")) },
3582                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, "fc815694-7977-4a27-a8b3-32b9d4075e4c", canonical(find_file(dir, "cpl_")) },
3583                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_SUBTITLE_NAMESPACE_COUNT, std::string{"315de731-1173-484c-9a35-bdacf5a9d99d"} }
3584                 });
3585 }
3586
3587
3588 BOOST_AUTO_TEST_CASE(verify_missing_load_font_for_font)
3589 {
3590         path const dir("build/test/verify_missing_load_font");
3591         prepare_directory (dir);
3592         copy_file ("test/data/subs1.xml", dir / "subs.xml");
3593         {
3594                 Editor editor(dir / "subs.xml");
3595                 editor.delete_first_line_containing("LoadFont");
3596         }
3597         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
3598         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
3599         write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
3600
3601         check_verify_result (
3602                 {dir}, {
3603                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
3604                         dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_LOAD_FONT_FOR_FONT).set_id("theFontId")
3605                 });
3606
3607 }
3608
3609
3610 BOOST_AUTO_TEST_CASE(verify_missing_load_font)
3611 {
3612         boost::filesystem::path const dir = "build/test/verify_missing_load_font";
3613         prepare_directory(dir);
3614         auto dcp = make_simple (dir, 1, 202);
3615
3616         string const xml =
3617                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3618                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\">"
3619                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
3620                 "<ContentTitleText>Content</ContentTitleText>"
3621                 "<AnnotationText>Annotation</AnnotationText>"
3622                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3623                 "<ReelNumber>1</ReelNumber>"
3624                 "<EditRate>24 1</EditRate>"
3625                 "<TimeCodeRate>24</TimeCodeRate>"
3626                 "<StartTime>00:00:00:00</StartTime>"
3627                 "<Language>de-DE</Language>"
3628                 "<SubtitleList>"
3629                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3630                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:06:00\" TimeOut=\"00:00:08:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3631                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3632                 "</Subtitle>"
3633                 "</Font>"
3634                 "</SubtitleList>"
3635                 "</SubtitleReel>";
3636
3637         dcp::File xml_file(dir / "subs.xml", "w");
3638         BOOST_REQUIRE(xml_file);
3639         xml_file.write(xml.c_str(), xml.size(), 1);
3640         xml_file.close();
3641         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
3642         subs->write(dir / "subs.mxf");
3643
3644         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 202, 0);
3645         dcp->cpls()[0]->reels()[0]->add(reel_subs);
3646         dcp->write_xml();
3647
3648         check_verify_result (
3649                 { dir },
3650                 {
3651                         dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_LOAD_FONT).set_id(reel_subs->id())
3652                 });
3653 }
3654
3655
3656 BOOST_AUTO_TEST_CASE(verify_spots_wrong_asset)
3657 {
3658         boost::filesystem::path const dir = "build/test/verify_spots_wrong_asset";
3659         boost::filesystem::remove_all(dir);
3660
3661         auto dcp1 = make_simple(dir / "1");
3662         dcp1->write_xml();
3663
3664         auto const asset_1 = dcp::MonoPictureAsset(dir / "1" / "video.mxf").id();
3665
3666         auto dcp2 = make_simple(dir / "2");
3667         dcp2->write_xml();
3668         auto const asset_2 = dcp::MonoPictureAsset(dir / "2" / "video.mxf").id();
3669
3670         boost::filesystem::remove(dir / "1" / "video.mxf");
3671         boost::filesystem::copy_file(dir / "2" / "video.mxf", dir / "1" / "video.mxf");
3672
3673         check_verify_result(
3674                 {dir / "1"},
3675                 {
3676                         dcp::VerificationNote(dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_MAP_ID).set_id(asset_1).set_other_id(asset_2)
3677                 });
3678 }
3679
3680
3681 BOOST_AUTO_TEST_CASE(verify_cpl_content_version_label_text_empty)
3682 {
3683         boost::filesystem::path const dir = "build/test/verify_cpl_content_version_label_text_empty";
3684         boost::filesystem::remove_all(dir);
3685
3686         auto dcp = make_simple(dir);
3687         BOOST_REQUIRE(dcp->cpls().size() == 1);
3688         auto cpl = dcp->cpls()[0];
3689         cpl->set_content_version(dcp::ContentVersion(""));
3690         dcp->write_xml();
3691
3692         check_verify_result(
3693                 {dir},
3694                 {
3695                         dcp::VerificationNote(dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_CONTENT_VERSION_LABEL_TEXT, cpl->file().get()).set_id(cpl->id())
3696                 });
3697 }