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