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