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