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