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