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