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