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