Allow specification of valign when writing verify tests.
[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 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
1622 {
1623         auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
1624         /* Just too early */
1625         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
1626         check_verify_result (
1627                 { dir },
1628                 {
1629                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1630                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1631                 });
1632
1633 }
1634
1635
1636 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time)
1637 {
1638         auto const dir = path("build/test/verify_valid_subtitle_first_text_time");
1639         /* Just late enough */
1640         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
1641         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1642 }
1643
1644
1645 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time_on_second_reel)
1646 {
1647         auto const dir = path("build/test/verify_valid_subtitle_first_text_time_on_second_reel");
1648         prepare_directory (dir);
1649
1650         auto asset1 = make_shared<dcp::SMPTESubtitleAsset>();
1651         asset1->set_start_time (dcp::Time());
1652         /* Just late enough */
1653         add_test_subtitle (asset1, 4 * 24, 5 * 24);
1654         asset1->set_language (dcp::LanguageTag("de-DE"));
1655         asset1->write (dir / "subs1.mxf");
1656         auto reel_asset1 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset1, dcp::Fraction(24, 1), 5 * 24, 0);
1657         auto reel1 = make_shared<dcp::Reel>();
1658         reel1->add (reel_asset1);
1659         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 5 * 24, 0);
1660         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
1661         reel1->add (markers1);
1662
1663         auto asset2 = make_shared<dcp::SMPTESubtitleAsset>();
1664         asset2->set_start_time (dcp::Time());
1665         /* This would be too early on first reel but should be OK on the second */
1666         add_test_subtitle (asset2, 3, 4 * 24);
1667         asset2->set_language (dcp::LanguageTag("de-DE"));
1668         asset2->write (dir / "subs2.mxf");
1669         auto reel_asset2 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset2, dcp::Fraction(24, 1), 4 * 24, 0);
1670         auto reel2 = make_shared<dcp::Reel>();
1671         reel2->add (reel_asset2);
1672         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 4 * 24, 0);
1673         markers2->set (dcp::Marker::LFOC, dcp::Time(4 * 24 - 1, 24, 24));
1674         reel2->add (markers2);
1675
1676         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1677         cpl->add (reel1);
1678         cpl->add (reel2);
1679         auto dcp = make_shared<dcp::DCP>(dir);
1680         dcp->add (cpl);
1681         dcp->write_xml (
1682                 dcp::String::compose("libdcp %1", dcp::version),
1683                 dcp::String::compose("libdcp %1", dcp::version),
1684                 dcp::LocalTime().as_string(),
1685                 "hello"
1686                 );
1687
1688
1689         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1690 }
1691
1692
1693 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_spacing)
1694 {
1695         auto const dir = path("build/test/verify_invalid_subtitle_spacing");
1696         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1697                 dir,
1698                 {
1699                         { 4 * 24,     5 * 24 },
1700                         { 5 * 24 + 1, 6 * 24 },
1701                 });
1702         check_verify_result (
1703                 {dir},
1704                 {
1705                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING },
1706                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1707                 });
1708 }
1709
1710
1711 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_spacing)
1712 {
1713         auto const dir = path("build/test/verify_valid_subtitle_spacing");
1714         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1715                 dir,
1716                 {
1717                         { 4 * 24,      5 * 24 },
1718                         { 5 * 24 + 16, 8 * 24 },
1719                 });
1720         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1721 }
1722
1723
1724 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_duration)
1725 {
1726         auto const dir = path("build/test/verify_invalid_subtitle_duration");
1727         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
1728         check_verify_result (
1729                 {dir},
1730                 {
1731                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION },
1732                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1733                 });
1734 }
1735
1736
1737 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_duration)
1738 {
1739         auto const dir = path("build/test/verify_valid_subtitle_duration");
1740         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
1741         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1742 }
1743
1744
1745 BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
1746 {
1747         auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
1748         prepare_directory (dir);
1749         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1750         asset->set_start_time (dcp::Time());
1751         add_test_subtitle (asset, 0, 4 * 24);
1752         asset->set_language (dcp::LanguageTag("de-DE"));
1753         asset->write (dir / "subs.mxf");
1754
1755         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
1756         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1757         check_verify_result (
1758                 {dir},
1759                 {
1760                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "72 96", boost::filesystem::canonical(asset->file().get()) },
1761                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1762                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
1763                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1764                 });
1765
1766 }
1767
1768
1769 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
1770 {
1771         auto const dir = path ("build/test/invalid_subtitle_line_count1");
1772         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1773                 dir,
1774                 {
1775                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1776                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1777                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1778                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1779                 });
1780         check_verify_result (
1781                 {dir},
1782                 {
1783                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1784                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1785                 });
1786 }
1787
1788
1789 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
1790 {
1791         auto const dir = path ("build/test/verify_valid_subtitle_line_count1");
1792         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1793                 dir,
1794                 {
1795                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1796                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1797                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1798                 });
1799         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1800 }
1801
1802
1803 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
1804 {
1805         auto const dir = path ("build/test/verify_invalid_subtitle_line_count2");
1806         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1807                 dir,
1808                 {
1809                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1810                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1811                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1812                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
1813                 });
1814         check_verify_result (
1815                 {dir},
1816                 {
1817                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1818                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1819                 });
1820 }
1821
1822
1823 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
1824 {
1825         auto const dir = path ("build/test/verify_valid_subtitle_line_count2");
1826         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1827                 dir,
1828                 {
1829                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1830                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1831                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1832                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
1833                 });
1834         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1835 }
1836
1837
1838 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
1839 {
1840         auto const dir = path ("build/test/verify_invalid_subtitle_line_length1");
1841         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1842                 dir,
1843                 {
1844                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123" }
1845                 });
1846         check_verify_result (
1847                 {dir},
1848                 {
1849                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH },
1850                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1851                 });
1852 }
1853
1854
1855 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
1856 {
1857         auto const dir = path ("build/test/verify_invalid_subtitle_line_length2");
1858         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1859                 dir,
1860                 {
1861                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
1862                 });
1863         check_verify_result (
1864                 {dir},
1865                 {
1866                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH },
1867                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1868                 });
1869 }
1870
1871
1872 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
1873 {
1874         auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
1875         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1876                 dir,
1877                 {
1878                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1879                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1880                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1881                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1882                 });
1883         check_verify_result (
1884                 {dir},
1885                 {
1886                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
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_valid_closed_caption_line_count2)
1893 {
1894         auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
1895         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1896                 dir,
1897                 {
1898                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1899                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1900                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1901                 });
1902         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1903 }
1904
1905
1906 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
1907 {
1908         auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
1909         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1910                 dir,
1911                 {
1912                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1913                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1914                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1915                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
1916                 });
1917         check_verify_result (
1918                 {dir},
1919                 {
1920                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
1921                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1922                 });
1923 }
1924
1925
1926 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
1927 {
1928         auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
1929         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1930                 dir,
1931                 {
1932                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1933                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1934                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1935                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
1936                 });
1937         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1938 }
1939
1940
1941 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
1942 {
1943         auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
1944         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1945                 dir,
1946                 {
1947                         { 96, 300, 0.0, dcp::VAlign::CENTER, "0123456789012345678901234567890123" }
1948                 });
1949         check_verify_result (
1950                 {dir},
1951                 {
1952                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH },
1953                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1954                 });
1955 }
1956
1957
1958 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
1959 {
1960         path const dir("build/test/verify_invalid_sound_frame_rate");
1961         prepare_directory (dir);
1962
1963         auto picture = simple_picture (dir, "foo");
1964         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
1965         auto reel = make_shared<dcp::Reel>();
1966         reel->add (reel_picture);
1967         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000, boost::none);
1968         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
1969         reel->add (reel_sound);
1970         reel->add (simple_markers());
1971         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1972         cpl->add (reel);
1973         auto dcp = make_shared<dcp::DCP>(dir);
1974         dcp->add (cpl);
1975         dcp->write_xml (
1976                 dcp::String::compose("libdcp %1", dcp::version),
1977                 dcp::String::compose("libdcp %1", dcp::version),
1978                 dcp::LocalTime().as_string(),
1979                 "hello"
1980                 );
1981
1982         check_verify_result (
1983                 {dir},
1984                 {
1985                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE, string("96000"), canonical(dir / "audiofoo.mxf") },
1986                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1987                 });
1988 }
1989
1990
1991 BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
1992 {
1993         path const dir("build/test/verify_missing_cpl_annotation_text");
1994         auto dcp = make_simple (dir);
1995         dcp->write_xml (
1996                 dcp::String::compose("libdcp %1", dcp::version),
1997                 dcp::String::compose("libdcp %1", dcp::version),
1998                 dcp::LocalTime().as_string(),
1999                 "A Test DCP"
2000                 );
2001
2002         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2003
2004         auto const cpl = dcp->cpls()[0];
2005
2006         {
2007                 BOOST_REQUIRE (cpl->file());
2008                 Editor e(cpl->file().get());
2009                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "");
2010         }
2011
2012         check_verify_result (
2013                 {dir},
2014                 {
2015                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2016                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2017                 });
2018 }
2019
2020
2021 BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
2022 {
2023         path const dir("build/test/verify_mismatched_cpl_annotation_text");
2024         auto dcp = make_simple (dir);
2025         dcp->write_xml (
2026                 dcp::String::compose("libdcp %1", dcp::version),
2027                 dcp::String::compose("libdcp %1", dcp::version),
2028                 dcp::LocalTime().as_string(),
2029                 "A Test DCP"
2030                 );
2031
2032         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2033         auto const cpl = dcp->cpls()[0];
2034
2035         {
2036                 BOOST_REQUIRE (cpl->file());
2037                 Editor e(cpl->file().get());
2038                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "<AnnotationText>A Test DCP 1</AnnotationText>");
2039         }
2040
2041         check_verify_result (
2042                 {dir},
2043                 {
2044                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2045                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2046                 });
2047 }
2048
2049
2050 BOOST_AUTO_TEST_CASE (verify_mismatched_asset_duration)
2051 {
2052         path const dir("build/test/verify_mismatched_asset_duration");
2053         prepare_directory (dir);
2054         shared_ptr<dcp::DCP> dcp (new dcp::DCP(dir));
2055         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2056
2057         shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (dir, "", 24);
2058         shared_ptr<dcp::SoundAsset> ms = simple_sound (dir, "", dcp::MXFMetadata(), "en-US", 25);
2059
2060         auto reel = make_shared<dcp::Reel>(
2061                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2062                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2063                 );
2064
2065         reel->add (simple_markers());
2066         cpl->add (reel);
2067
2068         dcp->add (cpl);
2069         dcp->write_xml (
2070                 dcp::String::compose("libdcp %1", dcp::version),
2071                 dcp::String::compose("libdcp %1", dcp::version),
2072                 dcp::LocalTime().as_string(),
2073                 "A Test DCP"
2074                 );
2075
2076         check_verify_result (
2077                 {dir},
2078                 {
2079                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION },
2080                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), canonical(cpl->file().get()) }
2081                 });
2082 }
2083
2084
2085
2086 static
2087 shared_ptr<dcp::CPL>
2088 verify_subtitles_must_be_in_all_reels_check (path dir, bool add_to_reel1, bool add_to_reel2)
2089 {
2090         prepare_directory (dir);
2091         auto dcp = make_shared<dcp::DCP>(dir);
2092         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2093
2094         auto constexpr reel_length = 192;
2095
2096         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2097         subs->set_language (dcp::LanguageTag("de-DE"));
2098         subs->set_start_time (dcp::Time());
2099         subs->add (simple_subtitle());
2100         subs->write (dir / "subs.mxf");
2101         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
2102
2103         auto reel1 = make_shared<dcp::Reel>(
2104                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2105                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2106                 );
2107
2108         if (add_to_reel1) {
2109                 reel1->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2110         }
2111
2112         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2113         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2114         reel1->add (markers1);
2115
2116         cpl->add (reel1);
2117
2118         auto reel2 = make_shared<dcp::Reel>(
2119                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2120                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2121                 );
2122
2123         if (add_to_reel2) {
2124                 reel2->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2125         }
2126
2127         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2128         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2129         reel2->add (markers2);
2130
2131         cpl->add (reel2);
2132
2133         dcp->add (cpl);
2134         dcp->write_xml (
2135                 dcp::String::compose("libdcp %1", dcp::version),
2136                 dcp::String::compose("libdcp %1", dcp::version),
2137                 dcp::LocalTime().as_string(),
2138                 "A Test DCP"
2139                 );
2140
2141         return cpl;
2142 }
2143
2144
2145 BOOST_AUTO_TEST_CASE (verify_missing_main_subtitle_from_some_reels)
2146 {
2147         {
2148                 path dir ("build/test/missing_main_subtitle_from_some_reels");
2149                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, false);
2150                 check_verify_result (
2151                         { dir },
2152                         {
2153                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS },
2154                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2155                         });
2156
2157         }
2158
2159         {
2160                 path dir ("build/test/verify_subtitles_must_be_in_all_reels2");
2161                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, true);
2162                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2163         }
2164
2165         {
2166                 path dir ("build/test/verify_subtitles_must_be_in_all_reels1");
2167                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, false, false);
2168                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2169         }
2170 }
2171
2172
2173 static
2174 shared_ptr<dcp::CPL>
2175 verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1, int caps_in_reel2)
2176 {
2177         prepare_directory (dir);
2178         auto dcp = make_shared<dcp::DCP>(dir);
2179         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2180
2181         auto constexpr reel_length = 192;
2182
2183         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2184         subs->set_language (dcp::LanguageTag("de-DE"));
2185         subs->set_start_time (dcp::Time());
2186         subs->add (simple_subtitle());
2187         subs->write (dir / "subs.mxf");
2188
2189         auto reel1 = make_shared<dcp::Reel>(
2190                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2191                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2192                 );
2193
2194         for (int i = 0; i < caps_in_reel1; ++i) {
2195                 reel1->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2196         }
2197
2198         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2199         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2200         reel1->add (markers1);
2201
2202         cpl->add (reel1);
2203
2204         auto reel2 = make_shared<dcp::Reel>(
2205                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2206                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2207                 );
2208
2209         for (int i = 0; i < caps_in_reel2; ++i) {
2210                 reel2->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2211         }
2212
2213         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length, 0);
2214         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2215         reel2->add (markers2);
2216
2217         cpl->add (reel2);
2218
2219         dcp->add (cpl);
2220         dcp->write_xml (
2221                 dcp::String::compose("libdcp %1", dcp::version),
2222                 dcp::String::compose("libdcp %1", dcp::version),
2223                 dcp::LocalTime().as_string(),
2224                 "A Test DCP"
2225                 );
2226
2227         return cpl;
2228 }
2229
2230
2231 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_asset_counts)
2232 {
2233         {
2234                 path dir ("build/test/mismatched_closed_caption_asset_counts");
2235                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 3, 4);
2236                 check_verify_result (
2237                         {dir},
2238                         {
2239                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS },
2240                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2241                         });
2242         }
2243
2244         {
2245                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels2");
2246                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 4, 4);
2247                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2248         }
2249
2250         {
2251                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels3");
2252                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 0, 0);
2253                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2254         }
2255 }
2256
2257
2258 template <class T>
2259 void
2260 verify_text_entry_point_check (path dir, dcp::VerificationNote::Code code, boost::function<void (shared_ptr<T>)> adjust)
2261 {
2262         prepare_directory (dir);
2263         auto dcp = make_shared<dcp::DCP>(dir);
2264         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2265
2266         auto constexpr reel_length = 192;
2267
2268         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2269         subs->set_language (dcp::LanguageTag("de-DE"));
2270         subs->set_start_time (dcp::Time());
2271         subs->add (simple_subtitle());
2272         subs->write (dir / "subs.mxf");
2273         auto reel_text = make_shared<T>(subs, dcp::Fraction(24, 1), reel_length, 0);
2274         adjust (reel_text);
2275
2276         auto reel = make_shared<dcp::Reel>(
2277                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2278                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2279                 );
2280
2281         reel->add (reel_text);
2282
2283         reel->add (simple_markers(reel_length));
2284
2285         cpl->add (reel);
2286
2287         dcp->add (cpl);
2288         dcp->write_xml (
2289                 dcp::String::compose("libdcp %1", dcp::version),
2290                 dcp::String::compose("libdcp %1", dcp::version),
2291                 dcp::LocalTime().as_string(),
2292                 "A Test DCP"
2293                 );
2294
2295         check_verify_result (
2296                 {dir},
2297                 {
2298                         { dcp::VerificationNote::Type::BV21_ERROR, code, subs->id() },
2299                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2300                 });
2301 }
2302
2303
2304 BOOST_AUTO_TEST_CASE (verify_text_entry_point)
2305 {
2306         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2307                 "build/test/verify_subtitle_entry_point_must_be_present",
2308                 dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT,
2309                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2310                         asset->unset_entry_point ();
2311                         }
2312                 );
2313
2314         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2315                 "build/test/verify_subtitle_entry_point_must_be_zero",
2316                 dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT,
2317                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2318                         asset->set_entry_point (4);
2319                         }
2320                 );
2321
2322         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2323                 "build/test/verify_closed_caption_entry_point_must_be_present",
2324                 dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
2325                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2326                         asset->unset_entry_point ();
2327                         }
2328                 );
2329
2330         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2331                 "build/test/verify_closed_caption_entry_point_must_be_zero",
2332                 dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
2333                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2334                         asset->set_entry_point (9);
2335                         }
2336                 );
2337 }
2338
2339
2340 BOOST_AUTO_TEST_CASE (verify_missing_hash)
2341 {
2342         RNGFixer fix;
2343
2344         path const dir("build/test/verify_missing_hash");
2345         auto dcp = make_simple (dir);
2346         dcp->write_xml (
2347                 dcp::String::compose("libdcp %1", dcp::version),
2348                 dcp::String::compose("libdcp %1", dcp::version),
2349                 dcp::LocalTime().as_string(),
2350                 "A Test DCP"
2351                 );
2352
2353         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2354         auto const cpl = dcp->cpls()[0];
2355         BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
2356         BOOST_REQUIRE (cpl->reels()[0]->main_picture());
2357         auto asset_id = cpl->reels()[0]->main_picture()->id();
2358
2359         {
2360                 BOOST_REQUIRE (cpl->file());
2361                 Editor e(cpl->file().get());
2362                 e.delete_first_line_containing("<Hash>");
2363         }
2364
2365         check_verify_result (
2366                 {dir},
2367                 {
2368                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2369                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
2370                 });
2371 }
2372
2373
2374 static
2375 void
2376 verify_markers_test (
2377         path dir,
2378         vector<pair<dcp::Marker, dcp::Time>> markers,
2379         vector<dcp::VerificationNote> test_notes
2380         )
2381 {
2382         auto dcp = make_simple (dir);
2383         dcp->cpls()[0]->set_content_kind (dcp::ContentKind::FEATURE);
2384         auto markers_asset = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 24, 0);
2385         for (auto const& i: markers) {
2386                 markers_asset->set (i.first, i.second);
2387         }
2388         dcp->cpls()[0]->reels()[0]->add(markers_asset);
2389         dcp->write_xml (
2390                 dcp::String::compose("libdcp %1", dcp::version),
2391                 dcp::String::compose("libdcp %1", dcp::version),
2392                 dcp::LocalTime().as_string(),
2393                 "A Test DCP"
2394                 );
2395
2396         check_verify_result ({dir}, test_notes);
2397 }
2398
2399
2400 BOOST_AUTO_TEST_CASE (verify_markers)
2401 {
2402         verify_markers_test (
2403                 "build/test/verify_markers_all_correct",
2404                 {
2405                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2406                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2407                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2408                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2409                 },
2410                 {}
2411                 );
2412
2413         verify_markers_test (
2414                 "build/test/verify_markers_missing_ffec",
2415                 {
2416                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2417                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2418                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2419                 },
2420                 {
2421                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE }
2422                 });
2423
2424         verify_markers_test (
2425                 "build/test/verify_markers_missing_ffmc",
2426                 {
2427                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2428                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2429                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2430                 },
2431                 {
2432                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
2433                 });
2434
2435         verify_markers_test (
2436                 "build/test/verify_markers_missing_ffoc",
2437                 {
2438                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2439                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2440                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2441                 },
2442                 {
2443                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC}
2444                 });
2445
2446         verify_markers_test (
2447                 "build/test/verify_markers_missing_lfoc",
2448                 {
2449                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2450                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2451                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) }
2452                 },
2453                 {
2454                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC }
2455                 });
2456
2457         verify_markers_test (
2458                 "build/test/verify_markers_incorrect_ffoc",
2459                 {
2460                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2461                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2462                         { dcp::Marker::FFOC, dcp::Time(3, 24, 24) },
2463                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2464                 },
2465                 {
2466                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_FFOC, string("3") }
2467                 });
2468
2469         verify_markers_test (
2470                 "build/test/verify_markers_incorrect_lfoc",
2471                 {
2472                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2473                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2474                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2475                         { dcp::Marker::LFOC, dcp::Time(18, 24, 24) }
2476                 },
2477                 {
2478                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_LFOC, string("18") }
2479                 });
2480 }
2481
2482
2483 BOOST_AUTO_TEST_CASE (verify_missing_cpl_metadata_version_number)
2484 {
2485         path dir = "build/test/verify_missing_cpl_metadata_version_number";
2486         prepare_directory (dir);
2487         auto dcp = make_simple (dir);
2488         auto cpl = dcp->cpls()[0];
2489         cpl->unset_version_number();
2490         dcp->write_xml (
2491                 dcp::String::compose("libdcp %1", dcp::version),
2492                 dcp::String::compose("libdcp %1", dcp::version),
2493                 dcp::LocalTime().as_string(),
2494                 "A Test DCP"
2495                 );
2496
2497         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get() }});
2498 }
2499
2500
2501 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
2502 {
2503         path dir = "build/test/verify_missing_extension_metadata1";
2504         auto dcp = make_simple (dir);
2505         dcp->write_xml (
2506                 dcp::String::compose("libdcp %1", dcp::version),
2507                 dcp::String::compose("libdcp %1", dcp::version),
2508                 dcp::LocalTime().as_string(),
2509                 "A Test DCP"
2510                 );
2511
2512         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2513         auto cpl = dcp->cpls()[0];
2514
2515         {
2516                 Editor e (cpl->file().get());
2517                 e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
2518         }
2519
2520         check_verify_result (
2521                 {dir},
2522                 {
2523                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2524                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2525                 });
2526 }
2527
2528
2529 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
2530 {
2531         path dir = "build/test/verify_missing_extension_metadata2";
2532         auto dcp = make_simple (dir);
2533         dcp->write_xml (
2534                 dcp::String::compose("libdcp %1", dcp::version),
2535                 dcp::String::compose("libdcp %1", dcp::version),
2536                 dcp::LocalTime().as_string(),
2537                 "A Test DCP"
2538                 );
2539
2540         auto cpl = dcp->cpls()[0];
2541
2542         {
2543                 Editor e (cpl->file().get());
2544                 e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
2545         }
2546
2547         check_verify_result (
2548                 {dir},
2549                 {
2550                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2551                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2552                 });
2553 }
2554
2555
2556 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
2557 {
2558         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata3";
2559         auto dcp = make_simple (dir);
2560         dcp->write_xml (
2561                 dcp::String::compose("libdcp %1", dcp::version),
2562                 dcp::String::compose("libdcp %1", dcp::version),
2563                 dcp::LocalTime().as_string(),
2564                 "A Test DCP"
2565                 );
2566
2567         auto const cpl = dcp->cpls()[0];
2568
2569         {
2570                 Editor e (cpl->file().get());
2571                 e.replace ("<meta:Name>A", "<meta:NameX>A");
2572                 e.replace ("n</meta:Name>", "n</meta:NameX>");
2573         }
2574
2575         check_verify_result (
2576                 {dir},
2577                 {
2578                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:NameX'"), cpl->file().get(), 75 },
2579                         { 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 },
2580                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2581                 });
2582 }
2583
2584
2585 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
2586 {
2587         path dir = "build/test/verify_invalid_extension_metadata1";
2588         auto dcp = make_simple (dir);
2589         dcp->write_xml (
2590                 dcp::String::compose("libdcp %1", dcp::version),
2591                 dcp::String::compose("libdcp %1", dcp::version),
2592                 dcp::LocalTime().as_string(),
2593                 "A Test DCP"
2594                 );
2595
2596         auto cpl = dcp->cpls()[0];
2597
2598         {
2599                 Editor e (cpl->file().get());
2600                 e.replace ("Application", "Fred");
2601         }
2602
2603         check_verify_result (
2604                 {dir},
2605                 {
2606                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2607                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
2608                 });
2609 }
2610
2611
2612 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
2613 {
2614         path dir = "build/test/verify_invalid_extension_metadata2";
2615         auto dcp = make_simple (dir);
2616         dcp->write_xml (
2617                 dcp::String::compose("libdcp %1", dcp::version),
2618                 dcp::String::compose("libdcp %1", dcp::version),
2619                 dcp::LocalTime().as_string(),
2620                 "A Test DCP"
2621                 );
2622
2623         auto cpl = dcp->cpls()[0];
2624
2625         {
2626                 Editor e (cpl->file().get());
2627                 e.replace ("DCP Constraints Profile", "Fred");
2628         }
2629
2630         check_verify_result (
2631                 {dir},
2632                 {
2633                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2634                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
2635                 });
2636 }
2637
2638
2639 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
2640 {
2641         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata6";
2642         auto dcp = make_simple (dir);
2643         dcp->write_xml (
2644                 dcp::String::compose("libdcp %1", dcp::version),
2645                 dcp::String::compose("libdcp %1", dcp::version),
2646                 dcp::LocalTime().as_string(),
2647                 "A Test DCP"
2648                 );
2649
2650         auto const cpl = dcp->cpls()[0];
2651
2652         {
2653                 Editor e (cpl->file().get());
2654                 e.replace ("<meta:Value>", "<meta:ValueX>");
2655                 e.replace ("</meta:Value>", "</meta:ValueX>");
2656         }
2657
2658         check_verify_result (
2659                 {dir},
2660                 {
2661                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 79 },
2662                         { 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 },
2663                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2664                 });
2665 }
2666
2667
2668 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
2669 {
2670         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata7";
2671         auto dcp = make_simple (dir);
2672         dcp->write_xml (
2673                 dcp::String::compose("libdcp %1", dcp::version),
2674                 dcp::String::compose("libdcp %1", dcp::version),
2675                 dcp::LocalTime().as_string(),
2676                 "A Test DCP"
2677                 );
2678
2679         auto const cpl = dcp->cpls()[0];
2680
2681         {
2682                 Editor e (cpl->file().get());
2683                 e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
2684         }
2685
2686         check_verify_result (
2687                 {dir},
2688                 {
2689                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2690                         { 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() },
2691                 });
2692 }
2693
2694
2695 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
2696 {
2697         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata8";
2698         auto dcp = make_simple (dir);
2699         dcp->write_xml (
2700                 dcp::String::compose("libdcp %1", dcp::version),
2701                 dcp::String::compose("libdcp %1", dcp::version),
2702                 dcp::LocalTime().as_string(),
2703                 "A Test DCP"
2704                 );
2705
2706         auto const cpl = dcp->cpls()[0];
2707
2708         {
2709                 Editor e (cpl->file().get());
2710                 e.replace ("<meta:Property>", "<meta:PropertyX>");
2711                 e.replace ("</meta:Property>", "</meta:PropertyX>");
2712         }
2713
2714         check_verify_result (
2715                 {dir},
2716                 {
2717                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 77 },
2718                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 81 },
2719                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2720                 });
2721 }
2722
2723
2724 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
2725 {
2726         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata9";
2727         auto dcp = make_simple (dir);
2728         dcp->write_xml (
2729                 dcp::String::compose("libdcp %1", dcp::version),
2730                 dcp::String::compose("libdcp %1", dcp::version),
2731                 dcp::LocalTime().as_string(),
2732                 "A Test DCP"
2733                 );
2734
2735         auto const cpl = dcp->cpls()[0];
2736
2737         {
2738                 Editor e (cpl->file().get());
2739                 e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
2740                 e.replace ("</meta:PropertyList>", "</meta:PropertyListX>");
2741         }
2742
2743         check_verify_result (
2744                 {dir},
2745                 {
2746                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 76 },
2747                         { 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 },
2748                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2749                 });
2750 }
2751
2752
2753
2754 BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
2755 {
2756         path dir = "build/test/verify_unsigned_cpl_with_encrypted_content";
2757         prepare_directory (dir);
2758         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2759                 copy_file (i.path(), dir / i.path().filename());
2760         }
2761
2762         path const pkl = dir / ( "pkl_" + encryption_test_pkl_id + ".xml" );
2763         path const cpl = dir / ( "cpl_" + encryption_test_cpl_id + ".xml");
2764
2765         {
2766                 Editor e (cpl);
2767                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2768         }
2769
2770         check_verify_result (
2771                 {dir},
2772                 {
2773                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id, canonical(cpl) },
2774                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl), },
2775                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2776                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2777                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2778                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2779                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2780                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, encryption_test_cpl_id, canonical(cpl) }
2781                 });
2782 }
2783
2784
2785 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_encrypted_content)
2786 {
2787         path dir = "build/test/unsigned_pkl_with_encrypted_content";
2788         prepare_directory (dir);
2789         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2790                 copy_file (i.path(), dir / i.path().filename());
2791         }
2792
2793         path const cpl = dir / ("cpl_" + encryption_test_cpl_id + ".xml");
2794         path const pkl = dir / ("pkl_" + encryption_test_pkl_id + ".xml");
2795         {
2796                 Editor e (pkl);
2797                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2798         }
2799
2800         check_verify_result (
2801                 {dir},
2802                 {
2803                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl) },
2804                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2805                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2806                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2807                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2808                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2809                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, encryption_test_pkl_id, canonical(pkl) },
2810                 });
2811 }
2812
2813
2814 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_unencrypted_content)
2815 {
2816         path dir = "build/test/verify_unsigned_pkl_with_unencrypted_content";
2817         prepare_directory (dir);
2818         for (auto i: directory_iterator("test/ref/DCP/dcp_test1")) {
2819                 copy_file (i.path(), dir / i.path().filename());
2820         }
2821
2822         {
2823                 Editor e (dir / dcp_test1_pkl);
2824                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2825         }
2826
2827         check_verify_result ({dir}, {});
2828 }
2829
2830
2831 BOOST_AUTO_TEST_CASE (verify_partially_encrypted)
2832 {
2833         path dir ("build/test/verify_must_not_be_partially_encrypted");
2834         prepare_directory (dir);
2835
2836         dcp::DCP d (dir);
2837
2838         auto signer = make_shared<dcp::CertificateChain>();
2839         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem")));
2840         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem")));
2841         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem")));
2842         signer->set_key (dcp::file_to_string("test/ref/crypt/leaf.key"));
2843
2844         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2845
2846         dcp::Key key;
2847
2848         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
2849         mp->set_key (key);
2850
2851         auto writer = mp->start_write (dir / "video.mxf", false);
2852         dcp::ArrayData j2c ("test/data/flat_red.j2c");
2853         for (int i = 0; i < 24; ++i) {
2854                 writer->write (j2c.data(), j2c.size());
2855         }
2856         writer->finalize ();
2857
2858         auto ms = simple_sound (dir, "", dcp::MXFMetadata(), "de-DE");
2859
2860         auto reel = make_shared<dcp::Reel>(
2861                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2862                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2863                 );
2864
2865         reel->add (simple_markers());
2866
2867         cpl->add (reel);
2868
2869         cpl->set_content_version (
2870                 {"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"}
2871                 );
2872         cpl->set_annotation_text ("A Test DCP");
2873         cpl->set_issuer ("OpenDCP 0.0.25");
2874         cpl->set_creator ("OpenDCP 0.0.25");
2875         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
2876         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
2877         cpl->set_main_sound_sample_rate (48000);
2878         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
2879         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
2880         cpl->set_version_number (1);
2881
2882         d.add (cpl);
2883
2884         d.write_xml ("OpenDCP 0.0.25", "OpenDCP 0.0.25", "2012-07-17T04:45:18+00:00", "A Test DCP", signer);
2885
2886         check_verify_result (
2887                 {dir},
2888                 {
2889                         {dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED},
2890                 });
2891 }
2892
2893
2894 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_2k)
2895 {
2896         vector<dcp::VerificationNote> notes;
2897         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"));
2898         auto reader = picture.start_read ();
2899         auto frame = reader->get_frame (0);
2900         verify_j2k (frame, notes);
2901         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2902 }
2903
2904
2905 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_4k)
2906 {
2907         vector<dcp::VerificationNote> notes;
2908         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "sul", "TLR"));
2909         auto reader = picture.start_read ();
2910         auto frame = reader->get_frame (0);
2911         verify_j2k (frame, notes);
2912         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2913 }
2914
2915
2916 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_libdcp)
2917 {
2918         boost::filesystem::path dir = "build/test/verify_jpeg2000_codestream_libdcp";
2919         prepare_directory (dir);
2920         auto dcp = make_simple (dir);
2921         dcp->write_xml ();
2922         vector<dcp::VerificationNote> notes;
2923         dcp::MonoPictureAsset picture (find_file(dir, "video"));
2924         auto reader = picture.start_read ();
2925         auto frame = reader->get_frame (0);
2926         verify_j2k (frame, notes);
2927         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
2928 }
2929
2930
2931 /** Check that ResourceID and the XML ID being different is spotted */
2932 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_resource_id)
2933 {
2934         boost::filesystem::path const dir = "build/test/verify_mismatched_subtitle_resource_id";
2935         prepare_directory (dir);
2936
2937         ASDCP::WriterInfo writer_info;
2938         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
2939
2940         unsigned int c;
2941         auto mxf_id = dcp::make_uuid ();
2942         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
2943         BOOST_REQUIRE (c == Kumu::UUID_Length);
2944
2945         auto resource_id = dcp::make_uuid ();
2946         ASDCP::TimedText::TimedTextDescriptor descriptor;
2947         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
2948         DCP_ASSERT (c == Kumu::UUID_Length);
2949
2950         auto xml_id = dcp::make_uuid ();
2951         ASDCP::TimedText::MXFWriter writer;
2952         auto subs_mxf = dir / "subs.mxf";
2953         auto r = writer.OpenWrite(subs_mxf.c_str(), writer_info, descriptor, 4096);
2954         BOOST_REQUIRE (ASDCP_SUCCESS(r));
2955         writer.WriteTimedTextResource (dcp::String::compose(
2956                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
2957                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
2958                 "<Id>urn:uuid:%1</Id>"
2959                 "<ContentTitleText>Content</ContentTitleText>"
2960                 "<AnnotationText>Annotation</AnnotationText>"
2961                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
2962                 "<ReelNumber>1</ReelNumber>"
2963                 "<Language>en-US</Language>"
2964                 "<EditRate>25 1</EditRate>"
2965                 "<TimeCodeRate>25</TimeCodeRate>"
2966                 "<StartTime>00:00:00:00</StartTime>"
2967                 "<SubtitleList>"
2968                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
2969                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
2970                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
2971                 "</Subtitle>"
2972                 "</Font>"
2973                 "</SubtitleList>"
2974                 "</SubtitleReel>",
2975                 xml_id).c_str());
2976
2977         writer.Finalize();
2978
2979         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
2980         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
2981
2982         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
2983
2984         check_verify_result (
2985                 { dir },
2986                 {
2987                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
2988                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID },
2989                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
2990                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2991                 });
2992 }
2993
2994
2995 /** Check that ResourceID and the MXF ID being the same is spotted */
2996 BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
2997 {
2998         boost::filesystem::path const dir = "build/test/verify_incorrect_timed_text_id";
2999         prepare_directory (dir);
3000
3001         ASDCP::WriterInfo writer_info;
3002         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3003
3004         unsigned int c;
3005         auto mxf_id = dcp::make_uuid ();
3006         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3007         BOOST_REQUIRE (c == Kumu::UUID_Length);
3008
3009         auto resource_id = mxf_id;
3010         ASDCP::TimedText::TimedTextDescriptor descriptor;
3011         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3012         DCP_ASSERT (c == Kumu::UUID_Length);
3013
3014         auto xml_id = resource_id;
3015         ASDCP::TimedText::MXFWriter writer;
3016         auto subs_mxf = dir / "subs.mxf";
3017         auto r = writer.OpenWrite(subs_mxf.c_str(), writer_info, descriptor, 4096);
3018         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3019         writer.WriteTimedTextResource (dcp::String::compose(
3020                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3021                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3022                 "<Id>urn:uuid:%1</Id>"
3023                 "<ContentTitleText>Content</ContentTitleText>"
3024                 "<AnnotationText>Annotation</AnnotationText>"
3025                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3026                 "<ReelNumber>1</ReelNumber>"
3027                 "<Language>en-US</Language>"
3028                 "<EditRate>25 1</EditRate>"
3029                 "<TimeCodeRate>25</TimeCodeRate>"
3030                 "<StartTime>00:00:00:00</StartTime>"
3031                 "<SubtitleList>"
3032                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3033                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3034                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3035                 "</Subtitle>"
3036                 "</Font>"
3037                 "</SubtitleList>"
3038                 "</SubtitleReel>",
3039                 xml_id).c_str());
3040
3041         writer.Finalize();
3042
3043         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3044         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3045
3046         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3047
3048         check_verify_result (
3049                 { dir },
3050                 {
3051                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3052                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID },
3053                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3054                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3055                 });
3056 }
3057
3058
3059 /** Check a DCP with a 3D asset marked as 2D */
3060 BOOST_AUTO_TEST_CASE (verify_threed_marked_as_twod)
3061 {
3062         check_verify_result (
3063                 { private_test / "data" / "xm" },
3064                 {
3065                         {
3066                                 dcp::VerificationNote::Type::WARNING,
3067                                 dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, boost::filesystem::canonical(find_file(private_test / "data" / "xm", "j2c"))
3068                         },
3069                         {
3070                                 dcp::VerificationNote::Type::BV21_ERROR,
3071                                 dcp::VerificationNote::Code::INVALID_STANDARD
3072                         },
3073                 });
3074
3075 }
3076