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