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