Check IssueDate for Deluxe complaint.
[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_interop_subtitle_asset_with_no_subtitles)
810 {
811         path const dir("build/test/verify_interop_subtitle_asset_with_no_subtitles");
812         prepare_directory(dir);
813         copy_file("test/data/subs4.xml", dir / "subs.xml");
814         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
815         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
816         write_dcp_with_single_asset(dir, reel_asset, dcp::Standard::INTEROP);
817
818         check_verify_result (
819                 { dir },
820                 {
821                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
822                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE, asset->id(), boost::filesystem::canonical(asset->file().get()) },
823                 });
824
825 }
826
827
828 BOOST_AUTO_TEST_CASE (verify_valid_smpte_subtitles)
829 {
830         path const dir("build/test/verify_valid_smpte_subtitles");
831         prepare_directory (dir);
832         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
833         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
834         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
835         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
836
837         check_verify_result(
838                 {dir},
839                 {
840                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
841                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, string{"2021-04-14T13:19:14.000+02:00"} }
842                 });
843 }
844
845
846 BOOST_AUTO_TEST_CASE (verify_invalid_smpte_subtitles)
847 {
848         using namespace boost::filesystem;
849
850         path const dir("build/test/verify_invalid_smpte_subtitles");
851         prepare_directory (dir);
852         /* This broken_smpte.mxf does not use urn:uuid: for its subtitle ID, which we tolerate (rightly or wrongly) */
853         copy_file ("test/data/broken_smpte.mxf", dir / "subs.mxf");
854         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
855         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
856         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
857
858         check_verify_result (
859                 { dir },
860                 {
861                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'Foo'"), path(), 2 },
862                         {
863                                 dcp::VerificationNote::Type::ERROR,
864                                 dcp::VerificationNote::Code::INVALID_XML,
865                                 string("element 'Foo' is not allowed for content model '(Id,ContentTitleText,AnnotationText?,IssueDate,ReelNumber?,Language?,EditRate,TimeCodeRate,StartTime?,DisplayType?,LoadFont*,SubtitleList)'"),
866                                 path(),
867                                 2
868                         },
869                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
870                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
871                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, string{"2020-05-09T00:29:21.000+02:00"} }
872                 });
873 }
874
875
876 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles)
877 {
878         path const dir("build/test/verify_empty_text_node_in_subtitles");
879         prepare_directory (dir);
880         copy_file ("test/data/empty_text.mxf", dir / "subs.mxf");
881         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
882         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
883         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
884
885         check_verify_result (
886                 { dir },
887                 {
888                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_TEXT },
889                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
890                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
891                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
892                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, string{"2021-08-09T18:34:46.000+02:00"} }
893                 });
894 }
895
896
897 /** A <Text> node with no content except some <Font> nodes, which themselves do have content */
898 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles_with_child_nodes)
899 {
900         path const dir("build/test/verify_empty_text_node_in_subtitles_with_child_nodes");
901         prepare_directory (dir);
902         copy_file ("test/data/empty_but_with_children.xml", dir / "subs.xml");
903         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
904         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
905         auto cpl = write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
906
907         check_verify_result (
908                 { dir },
909                 {
910                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
911                 });
912 }
913
914
915 /** A <Text> node with no content except some <Font> nodes, which themselves also have no content */
916 BOOST_AUTO_TEST_CASE (verify_empty_text_node_in_subtitles_with_empty_child_nodes)
917 {
918         path const dir("build/test/verify_empty_text_node_in_subtitles_with_empty_child_nodes");
919         prepare_directory (dir);
920         copy_file ("test/data/empty_with_empty_children.xml", dir / "subs.xml");
921         auto asset = make_shared<dcp::InteropSubtitleAsset>(dir / "subs.xml");
922         auto reel_asset = make_shared<dcp::ReelInteropSubtitleAsset>(asset, dcp::Fraction(24, 1), 192, 0);
923         auto cpl = write_dcp_with_single_asset (dir, reel_asset, dcp::Standard::INTEROP);
924
925         check_verify_result (
926                 { dir },
927                 {
928                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE, asset->id(), boost::filesystem::canonical(asset->file().get()) },
929                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_STANDARD },
930                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EMPTY_TEXT },
931                 });
932 }
933
934
935 BOOST_AUTO_TEST_CASE (verify_external_asset)
936 {
937         path const ov_dir("build/test/verify_external_asset");
938         prepare_directory (ov_dir);
939
940         auto image = black_image ();
941         auto frame = dcp::compress_j2k (image, 100000000, 24, false, false);
942         BOOST_REQUIRE (frame.size() < 230000000 / (24 * 8));
943         dcp_from_frame (frame, ov_dir);
944
945         dcp::DCP ov (ov_dir);
946         ov.read ();
947
948         path const vf_dir("build/test/verify_external_asset_vf");
949         prepare_directory (vf_dir);
950
951         auto picture = ov.cpls()[0]->reels()[0]->main_picture();
952         auto cpl = write_dcp_with_single_asset (vf_dir, picture);
953
954         check_verify_result (
955                 { vf_dir },
956                 {
957                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, picture->asset()->id() },
958                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
959                 });
960 }
961
962
963 BOOST_AUTO_TEST_CASE (verify_valid_cpl_metadata)
964 {
965         path const dir("build/test/verify_valid_cpl_metadata");
966         prepare_directory (dir);
967
968         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
969         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
970         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 16 * 24, 0);
971
972         auto reel = make_shared<dcp::Reel>();
973         reel->add (reel_asset);
974
975         reel->add (make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", 16 * 24), 0));
976         reel->add (simple_markers(16 * 24));
977
978         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
979         cpl->add (reel);
980         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
981         cpl->set_main_sound_sample_rate (48000);
982         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
983         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
984         cpl->set_version_number (1);
985
986         dcp::DCP dcp (dir);
987         dcp.add (cpl);
988         dcp.set_annotation_text("hello");
989         dcp.write_xml ();
990 }
991
992
993 path
994 find_prefix(path dir, string prefix)
995 {
996         auto iter = std::find_if(directory_iterator(dir), directory_iterator(), [prefix](path const& p) {
997                 return boost::starts_with(p.filename().string(), prefix);
998         });
999
1000         BOOST_REQUIRE(iter != directory_iterator());
1001         return iter->path();
1002 }
1003
1004
1005 path find_cpl (path dir)
1006 {
1007         return find_prefix(dir, "cpl_");
1008 }
1009
1010
1011 path
1012 find_pkl(path dir)
1013 {
1014         return find_prefix(dir, "pkl_");
1015 }
1016
1017
1018 path
1019 find_asset_map(path dir)
1020 {
1021         return find_prefix(dir, "ASSETMAP");
1022 }
1023
1024
1025 /* DCP with invalid CompositionMetadataAsset */
1026 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_bad_tag)
1027 {
1028         using namespace boost::filesystem;
1029
1030         path const dir("build/test/verify_invalid_cpl_metadata_bad_tag");
1031         prepare_directory (dir);
1032
1033         auto reel = make_shared<dcp::Reel>();
1034         reel->add (black_picture_asset(dir));
1035         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1036         cpl->add (reel);
1037         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1038         cpl->set_main_sound_sample_rate (48000);
1039         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1040         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1041         cpl->set_version_number (1);
1042
1043         reel->add (simple_markers());
1044
1045         dcp::DCP dcp (dir);
1046         dcp.add (cpl);
1047         dcp.set_annotation_text("hello");
1048         dcp.write_xml();
1049
1050         {
1051                 Editor e (find_cpl(dir));
1052                 e.replace ("MainSound", "MainSoundX");
1053         }
1054
1055         check_verify_result (
1056                 { dir },
1057                 {
1058                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXConfiguration'"), canonical(cpl->file().get()), 50 },
1059                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:MainSoundXSampleRate'"), canonical(cpl->file().get()), 51 },
1060                         {
1061                                 dcp::VerificationNote::Type::ERROR,
1062                                 dcp::VerificationNote::Code::INVALID_XML,
1063                                 string("element 'meta:MainSoundXConfiguration' is not allowed for content model "
1064                                        "'(Id,AnnotationText?,EditRate,IntrinsicDuration,EntryPoint?,Duration?,"
1065                                        "FullContentTitleText,ReleaseTerritory?,VersionNumber?,Chain?,Distributor?,"
1066                                        "Facility?,AlternateContentVersionList?,Luminance?,MainSoundConfiguration,"
1067                                        "MainSoundSampleRate,MainPictureStoredArea,MainPictureActiveArea,MainSubtitleLanguageList?,"
1068                                        "ExtensionMetadataList?,)'"),
1069                                 canonical(cpl->file().get()),
1070                                 71
1071                         },
1072                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) },
1073                 });
1074 }
1075
1076
1077 /* DCP with invalid CompositionMetadataAsset */
1078 BOOST_AUTO_TEST_CASE (verify_invalid_cpl_metadata_missing_tag)
1079 {
1080         path const dir("build/test/verify_invalid_cpl_metadata_missing_tag");
1081         prepare_directory (dir);
1082
1083         auto reel = make_shared<dcp::Reel>();
1084         reel->add (black_picture_asset(dir));
1085         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1086         cpl->add (reel);
1087         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1088         cpl->set_main_sound_sample_rate (48000);
1089         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1090         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1091
1092         dcp::DCP dcp (dir);
1093         dcp.add (cpl);
1094         dcp.set_annotation_text("hello");
1095         dcp.write_xml();
1096
1097         {
1098                 Editor e (find_cpl(dir));
1099                 e.replace ("meta:Width", "meta:WidthX");
1100         }
1101
1102         check_verify_result (
1103                 { dir },
1104                 {{ dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::FAILED_READ, string("missing XML tag Width in MainPictureStoredArea") }}
1105                 );
1106 }
1107
1108
1109 BOOST_AUTO_TEST_CASE (verify_invalid_language1)
1110 {
1111         path const dir("build/test/verify_invalid_language1");
1112         prepare_directory (dir);
1113         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
1114         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
1115         asset->_language = "wrong-andbad";
1116         asset->write (dir / "subs.mxf");
1117         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
1118         reel_asset->_language = "badlang";
1119         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1120
1121         check_verify_result (
1122                 { dir },
1123                 {
1124                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
1125                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
1126                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1127                 });
1128 }
1129
1130
1131 /* SMPTE DCP with invalid <Language> in the MainClosedCaption reel and also in the XML within the MXF */
1132 BOOST_AUTO_TEST_CASE (verify_invalid_language2)
1133 {
1134         path const dir("build/test/verify_invalid_language2");
1135         prepare_directory (dir);
1136         copy_file ("test/data/subs.mxf", dir / "subs.mxf");
1137         auto asset = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.mxf");
1138         asset->_language = "wrong-andbad";
1139         asset->write (dir / "subs.mxf");
1140         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 6046, 0);
1141         reel_asset->_language = "badlang";
1142         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1143
1144         check_verify_result (
1145                 {dir},
1146                 {
1147                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("badlang") },
1148                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("wrong-andbad") },
1149                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1150                 });
1151 }
1152
1153
1154 /* SMPTE DCP with invalid <Language> in the MainSound reel, the CPL additional subtitles languages and
1155  * the release territory.
1156  */
1157 BOOST_AUTO_TEST_CASE (verify_invalid_language3)
1158 {
1159         path const dir("build/test/verify_invalid_language3");
1160         prepare_directory (dir);
1161
1162         auto picture = simple_picture (dir, "foo");
1163         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
1164         auto reel = make_shared<dcp::Reel>();
1165         reel->add (reel_picture);
1166         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "frobozz");
1167         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
1168         reel->add (reel_sound);
1169         reel->add (simple_markers());
1170
1171         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1172         cpl->add (reel);
1173         cpl->_additional_subtitle_languages.push_back("this-is-wrong");
1174         cpl->_additional_subtitle_languages.push_back("andso-is-this");
1175         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1176         cpl->set_main_sound_sample_rate (48000);
1177         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
1178         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
1179         cpl->set_version_number (1);
1180         cpl->_release_territory = "fred-jim";
1181         auto dcp = make_shared<dcp::DCP>(dir);
1182         dcp->add (cpl);
1183         dcp->set_annotation_text("hello");
1184         dcp->write_xml();
1185
1186         check_verify_result (
1187                 { dir },
1188                 {
1189                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("this-is-wrong") },
1190                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("andso-is-this") },
1191                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("fred-jim") },
1192                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_LANGUAGE, string("frobozz") },
1193                 });
1194 }
1195
1196
1197 static
1198 vector<dcp::VerificationNote>
1199 check_picture_size (int width, int height, int frame_rate, bool three_d)
1200 {
1201         using namespace boost::filesystem;
1202
1203         path dcp_path = "build/test/verify_picture_test";
1204         prepare_directory (dcp_path);
1205
1206         shared_ptr<dcp::PictureAsset> mp;
1207         if (three_d) {
1208                 mp = make_shared<dcp::StereoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1209         } else {
1210                 mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction(frame_rate, 1), dcp::Standard::SMPTE);
1211         }
1212         auto picture_writer = mp->start_write (dcp_path / "video.mxf", false);
1213
1214         auto image = black_image (dcp::Size(width, height));
1215         auto j2c = dcp::compress_j2k (image, 100000000, frame_rate, three_d, width > 2048);
1216         int const length = three_d ? frame_rate * 2 : frame_rate;
1217         for (int i = 0; i < length; ++i) {
1218                 picture_writer->write (j2c.data(), j2c.size());
1219         }
1220         picture_writer->finalize ();
1221
1222         auto d = make_shared<dcp::DCP>(dcp_path);
1223         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1224         cpl->set_annotation_text ("A Test DCP");
1225         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
1226         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
1227         cpl->set_main_sound_sample_rate (48000);
1228         cpl->set_main_picture_stored_area(dcp::Size(width, height));
1229         cpl->set_main_picture_active_area(dcp::Size(width, height));
1230         cpl->set_version_number (1);
1231
1232         auto reel = make_shared<dcp::Reel>();
1233
1234         if (three_d) {
1235                 reel->add (make_shared<dcp::ReelStereoPictureAsset>(std::dynamic_pointer_cast<dcp::StereoPictureAsset>(mp), 0));
1236         } else {
1237                 reel->add (make_shared<dcp::ReelMonoPictureAsset>(std::dynamic_pointer_cast<dcp::MonoPictureAsset>(mp), 0));
1238         }
1239
1240         reel->add (simple_markers(frame_rate));
1241
1242         cpl->add (reel);
1243
1244         d->add (cpl);
1245         d->set_annotation_text("A Test DCP");
1246         d->write_xml();
1247
1248         return dcp::verify ({dcp_path}, &stage, &progress, xsd_test);
1249 }
1250
1251
1252 static
1253 void
1254 check_picture_size_ok (int width, int height, int frame_rate, bool three_d)
1255 {
1256         auto notes = check_picture_size(width, height, frame_rate, three_d);
1257         BOOST_CHECK_EQUAL (notes.size(), 0U);
1258 }
1259
1260
1261 static
1262 void
1263 check_picture_size_bad_frame_size (int width, int height, int frame_rate, bool three_d)
1264 {
1265         auto notes = check_picture_size(width, height, frame_rate, three_d);
1266         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1267         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1268         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_SIZE_IN_PIXELS);
1269 }
1270
1271
1272 static
1273 void
1274 check_picture_size_bad_2k_frame_rate (int width, int height, int frame_rate, bool three_d)
1275 {
1276         auto notes = check_picture_size(width, height, frame_rate, three_d);
1277         BOOST_REQUIRE_EQUAL (notes.size(), 2U);
1278         BOOST_CHECK_EQUAL (notes.back().type(), dcp::VerificationNote::Type::BV21_ERROR);
1279         BOOST_CHECK_EQUAL (notes.back().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_2K);
1280 }
1281
1282
1283 static
1284 void
1285 check_picture_size_bad_4k_frame_rate (int width, int height, int frame_rate, bool three_d)
1286 {
1287         auto notes = check_picture_size(width, height, frame_rate, three_d);
1288         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1289         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1290         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_FRAME_RATE_FOR_4K);
1291 }
1292
1293
1294 BOOST_AUTO_TEST_CASE (verify_picture_size)
1295 {
1296         using namespace boost::filesystem;
1297
1298         /* 2K scope */
1299         check_picture_size_ok (2048, 858, 24, false);
1300         check_picture_size_ok (2048, 858, 25, false);
1301         check_picture_size_ok (2048, 858, 48, false);
1302         check_picture_size_ok (2048, 858, 24, true);
1303         check_picture_size_ok (2048, 858, 25, true);
1304         check_picture_size_ok (2048, 858, 48, true);
1305
1306         /* 2K flat */
1307         check_picture_size_ok (1998, 1080, 24, false);
1308         check_picture_size_ok (1998, 1080, 25, false);
1309         check_picture_size_ok (1998, 1080, 48, false);
1310         check_picture_size_ok (1998, 1080, 24, true);
1311         check_picture_size_ok (1998, 1080, 25, true);
1312         check_picture_size_ok (1998, 1080, 48, true);
1313
1314         /* 4K scope */
1315         check_picture_size_ok (4096, 1716, 24, false);
1316
1317         /* 4K flat */
1318         check_picture_size_ok (3996, 2160, 24, false);
1319
1320         /* Bad frame size */
1321         check_picture_size_bad_frame_size (2050, 858, 24, false);
1322         check_picture_size_bad_frame_size (2048, 658, 25, false);
1323         check_picture_size_bad_frame_size (1920, 1080, 48, true);
1324         check_picture_size_bad_frame_size (4000, 2000, 24, true);
1325
1326         /* Bad 2K frame rate */
1327         check_picture_size_bad_2k_frame_rate (2048, 858, 26, false);
1328         check_picture_size_bad_2k_frame_rate (2048, 858, 31, false);
1329         check_picture_size_bad_2k_frame_rate (1998, 1080, 50, true);
1330
1331         /* Bad 4K frame rate */
1332         check_picture_size_bad_4k_frame_rate (3996, 2160, 25, false);
1333         check_picture_size_bad_4k_frame_rate (3996, 2160, 48, false);
1334
1335         /* No 4K 3D */
1336         auto notes = check_picture_size(3996, 2160, 24, true);
1337         BOOST_REQUIRE_EQUAL (notes.size(), 1U);
1338         BOOST_CHECK_EQUAL (notes.front().type(), dcp::VerificationNote::Type::BV21_ERROR);
1339         BOOST_CHECK_EQUAL (notes.front().code(), dcp::VerificationNote::Code::INVALID_PICTURE_ASSET_RESOLUTION_FOR_3D);
1340 }
1341
1342
1343 static
1344 void
1345 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")
1346 {
1347         asset->add (
1348                 std::make_shared<dcp::SubtitleString>(
1349                         optional<string>(),
1350                         false,
1351                         false,
1352                         false,
1353                         dcp::Colour(),
1354                         42,
1355                         1,
1356                         dcp::Time(start_frame, 24, 24),
1357                         dcp::Time(end_frame, 24, 24),
1358                         0,
1359                         dcp::HAlign::CENTER,
1360                         v_position,
1361                         v_align,
1362                         0,
1363                         dcp::Direction::LTR,
1364                         text,
1365                         dcp::Effect::NONE,
1366                         dcp::Colour(),
1367                         dcp::Time(),
1368                         dcp::Time(),
1369                         0
1370                 )
1371         );
1372 }
1373
1374
1375 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_xml_size_in_bytes)
1376 {
1377         path const dir("build/test/verify_invalid_closed_caption_xml_size_in_bytes");
1378         prepare_directory (dir);
1379
1380         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1381         for (int i = 0; i < 2048; ++i) {
1382                 add_test_subtitle (asset, i * 24, i * 24 + 20);
1383         }
1384         asset->set_language (dcp::LanguageTag("de-DE"));
1385         asset->write (dir / "subs.mxf");
1386         auto reel_asset = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(asset, dcp::Fraction(24, 1), 49148, 0);
1387         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1388
1389         check_verify_result (
1390                 { dir },
1391                 {
1392                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1393                         {
1394                                 dcp::VerificationNote::Type::BV21_ERROR,
1395                                 dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_XML_SIZE_IN_BYTES,
1396                                 string("419336"),
1397                                 canonical(dir / "subs.mxf")
1398                         },
1399                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1400                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1401                 });
1402 }
1403
1404
1405 static
1406 shared_ptr<dcp::SMPTESubtitleAsset>
1407 make_large_subtitle_asset (path font_file)
1408 {
1409         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1410         dcp::ArrayData big_fake_font(1024 * 1024);
1411         big_fake_font.write (font_file);
1412         for (int i = 0; i < 116; ++i) {
1413                 asset->add_font (dcp::String::compose("big%1", i), big_fake_font);
1414         }
1415         return asset;
1416 }
1417
1418
1419 template <class T>
1420 void
1421 verify_timed_text_asset_too_large (string name)
1422 {
1423         auto const dir = path("build/test") / name;
1424         prepare_directory (dir);
1425         auto asset = make_large_subtitle_asset (dir / "font.ttf");
1426         add_test_subtitle (asset, 0, 240);
1427         asset->set_language (dcp::LanguageTag("de-DE"));
1428         asset->write (dir / "subs.mxf");
1429
1430         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), 240, 0);
1431         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1432
1433         check_verify_result (
1434                 { dir },
1435                 {
1436                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_SIZE_IN_BYTES, string("121695532"), canonical(dir / "subs.mxf") },
1437                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_TIMED_TEXT_FONT_SIZE_IN_BYTES, string("121634816"), canonical(dir / "subs.mxf") },
1438                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1439                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1440                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
1441                 });
1442 }
1443
1444
1445 BOOST_AUTO_TEST_CASE (verify_subtitle_asset_too_large)
1446 {
1447         verify_timed_text_asset_too_large<dcp::ReelSMPTESubtitleAsset>("verify_subtitle_asset_too_large");
1448         verify_timed_text_asset_too_large<dcp::ReelSMPTEClosedCaptionAsset>("verify_closed_caption_asset_too_large");
1449 }
1450
1451
1452 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_language)
1453 {
1454         path dir = "build/test/verify_missing_subtitle_language";
1455         prepare_directory (dir);
1456         auto dcp = make_simple (dir, 1, 106);
1457
1458         string const xml =
1459                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1460                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1461                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1462                 "<ContentTitleText>Content</ContentTitleText>"
1463                 "<AnnotationText>Annotation</AnnotationText>"
1464                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1465                 "<ReelNumber>1</ReelNumber>"
1466                 "<EditRate>24 1</EditRate>"
1467                 "<TimeCodeRate>24</TimeCodeRate>"
1468                 "<StartTime>00:00:00:00</StartTime>"
1469                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1470                 "<SubtitleList>"
1471                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1472                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1473                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1474                 "</Subtitle>"
1475                 "</Font>"
1476                 "</SubtitleList>"
1477                 "</SubtitleReel>";
1478
1479         dcp::File xml_file(dir / "subs.xml", "w");
1480         BOOST_REQUIRE (xml_file);
1481         xml_file.write(xml.c_str(), xml.size(), 1);
1482         xml_file.close();
1483         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1484         subs->write (dir / "subs.mxf");
1485
1486         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1487         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1488         dcp->set_annotation_text("A Test DCP");
1489         dcp->write_xml();
1490
1491         check_verify_result (
1492                 { dir },
1493                 {
1494                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_LANGUAGE, canonical(dir / "subs.mxf") },
1495                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1496                 });
1497 }
1498
1499
1500 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_languages)
1501 {
1502         path path ("build/test/verify_mismatched_subtitle_languages");
1503         auto constexpr reel_length = 192;
1504         auto dcp = make_simple (path, 2, reel_length);
1505         auto cpl = dcp->cpls()[0];
1506
1507         {
1508                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1509                 subs->set_language (dcp::LanguageTag("de-DE"));
1510                 subs->add (simple_subtitle());
1511                 subs->write (path / "subs1.mxf");
1512                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1513                 cpl->reels()[0]->add(reel_subs);
1514         }
1515
1516         {
1517                 auto subs = make_shared<dcp::SMPTESubtitleAsset>();
1518                 subs->set_language (dcp::LanguageTag("en-US"));
1519                 subs->add (simple_subtitle());
1520                 subs->write (path / "subs2.mxf");
1521                 auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
1522                 cpl->reels()[1]->add(reel_subs);
1523         }
1524
1525         dcp->set_annotation_text("A Test DCP");
1526         dcp->write_xml();
1527
1528         check_verify_result (
1529                 { path },
1530                 {
1531                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1532                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") },
1533                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_SUBTITLE_LANGUAGES }
1534                 });
1535 }
1536
1537
1538 BOOST_AUTO_TEST_CASE (verify_multiple_closed_caption_languages_allowed)
1539 {
1540         path path ("build/test/verify_multiple_closed_caption_languages_allowed");
1541         auto constexpr reel_length = 192;
1542         auto dcp = make_simple (path, 2, reel_length);
1543         auto cpl = dcp->cpls()[0];
1544
1545         {
1546                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1547                 ccaps->set_language (dcp::LanguageTag("de-DE"));
1548                 ccaps->add (simple_subtitle());
1549                 ccaps->write (path / "subs1.mxf");
1550                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1551                 cpl->reels()[0]->add(reel_ccaps);
1552         }
1553
1554         {
1555                 auto ccaps = make_shared<dcp::SMPTESubtitleAsset>();
1556                 ccaps->set_language (dcp::LanguageTag("en-US"));
1557                 ccaps->add (simple_subtitle());
1558                 ccaps->write (path / "subs2.mxf");
1559                 auto reel_ccaps = make_shared<dcp::ReelSMPTEClosedCaptionAsset>(ccaps, dcp::Fraction(24, 1), reel_length, 0);
1560                 cpl->reels()[1]->add(reel_ccaps);
1561         }
1562
1563         dcp->set_annotation_text("A Test DCP");
1564         dcp->write_xml();
1565
1566         check_verify_result (
1567                 { path },
1568                 {
1569                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs1.mxf") },
1570                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(path / "subs2.mxf") }
1571                 });
1572 }
1573
1574
1575 BOOST_AUTO_TEST_CASE (verify_missing_subtitle_start_time)
1576 {
1577         path dir = "build/test/verify_missing_subtitle_start_time";
1578         prepare_directory (dir);
1579         auto dcp = make_simple (dir, 1, 106);
1580
1581         string const xml =
1582                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1583                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1584                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1585                 "<ContentTitleText>Content</ContentTitleText>"
1586                 "<AnnotationText>Annotation</AnnotationText>"
1587                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1588                 "<ReelNumber>1</ReelNumber>"
1589                 "<Language>de-DE</Language>"
1590                 "<EditRate>24 1</EditRate>"
1591                 "<TimeCodeRate>24</TimeCodeRate>"
1592                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1593                 "<SubtitleList>"
1594                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1595                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1596                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1597                 "</Subtitle>"
1598                 "</Font>"
1599                 "</SubtitleList>"
1600                 "</SubtitleReel>";
1601
1602         dcp::File xml_file(dir / "subs.xml", "w");
1603         BOOST_REQUIRE (xml_file);
1604         xml_file.write(xml.c_str(), xml.size(), 1);
1605         xml_file.close();
1606         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1607         subs->write (dir / "subs.mxf");
1608
1609         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1610         dcp->cpls()[0]->reels()[0]->add(reel_subs);
1611         dcp->set_annotation_text("A Test DCP");
1612         dcp->write_xml();
1613
1614         check_verify_result (
1615                 { dir },
1616                 {
1617                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1618                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1619                 });
1620 }
1621
1622
1623 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_start_time)
1624 {
1625         path dir = "build/test/verify_invalid_subtitle_start_time";
1626         prepare_directory (dir);
1627         auto dcp = make_simple (dir, 1, 106);
1628
1629         string const xml =
1630                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1631                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
1632                 "<Id>urn:uuid:e6a8ae03-ebbf-41ed-9def-913a87d1493a</Id>"
1633                 "<ContentTitleText>Content</ContentTitleText>"
1634                 "<AnnotationText>Annotation</AnnotationText>"
1635                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
1636                 "<ReelNumber>1</ReelNumber>"
1637                 "<Language>de-DE</Language>"
1638                 "<EditRate>24 1</EditRate>"
1639                 "<TimeCodeRate>24</TimeCodeRate>"
1640                 "<StartTime>00:00:02:00</StartTime>"
1641                 "<LoadFont ID=\"arial\">urn:uuid:e4f0ff0a-9eba-49e0-92ee-d89a88a575f6</LoadFont>"
1642                 "<SubtitleList>"
1643                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
1644                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
1645                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
1646                 "</Subtitle>"
1647                 "</Font>"
1648                 "</SubtitleList>"
1649                 "</SubtitleReel>";
1650
1651         dcp::File xml_file(dir / "subs.xml", "w");
1652         BOOST_REQUIRE (xml_file);
1653         xml_file.write(xml.c_str(), xml.size(), 1);
1654         xml_file.close();
1655         auto subs = make_shared<dcp::SMPTESubtitleAsset>(dir / "subs.xml");
1656         subs->write (dir / "subs.mxf");
1657
1658         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), 106, 0);
1659         dcp->cpls().front()->reels().front()->add(reel_subs);
1660         dcp->set_annotation_text("A Test DCP");
1661         dcp->write_xml();
1662
1663         check_verify_result (
1664                 { dir },
1665                 {
1666                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SUBTITLE_START_TIME, canonical(dir / "subs.mxf") },
1667                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME }
1668                 });
1669 }
1670
1671
1672 class TestText
1673 {
1674 public:
1675         TestText (int in_, int out_, float v_position_ = 0, dcp::VAlign v_align_ = dcp::VAlign::CENTER, string text_ = "Hello")
1676                 : in(in_)
1677                 , out(out_)
1678                 , v_position(v_position_)
1679                 , v_align(v_align_)
1680                 , text(text_)
1681         {}
1682
1683         int in;
1684         int out;
1685         float v_position;
1686         dcp::VAlign v_align;
1687         string text;
1688 };
1689
1690
1691 template <class T>
1692 shared_ptr<dcp::CPL>
1693 dcp_with_text (path dir, vector<TestText> subs)
1694 {
1695         prepare_directory (dir);
1696         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1697         asset->set_start_time (dcp::Time());
1698         for (auto i: subs) {
1699                 add_test_subtitle (asset, i.in, i.out, i.v_position, i.v_align, i.text);
1700         }
1701         asset->set_language (dcp::LanguageTag("de-DE"));
1702         asset->write (dir / "subs.mxf");
1703
1704         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1705         return write_dcp_with_single_asset (dir, reel_asset);
1706 }
1707
1708
1709 template <class T>
1710 shared_ptr<dcp::CPL>
1711 dcp_with_text_from_file (path dir, boost::filesystem::path subs_xml)
1712 {
1713         prepare_directory (dir);
1714         auto asset = make_shared<dcp::SMPTESubtitleAsset>(subs_xml);
1715         asset->set_start_time (dcp::Time());
1716         asset->set_language (dcp::LanguageTag("de-DE"));
1717
1718         auto subs_mxf = dir / "subs.mxf";
1719         asset->write (subs_mxf);
1720
1721         /* The call to write() puts the asset into the DCP correctly but it will have
1722          * XML re-written by our parser.  Overwrite the MXF using the given file's verbatim
1723          * contents.
1724          */
1725         ASDCP::TimedText::MXFWriter writer;
1726         ASDCP::WriterInfo writer_info;
1727         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
1728         unsigned int c;
1729         Kumu::hex2bin (asset->id().c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
1730         DCP_ASSERT (c == Kumu::UUID_Length);
1731         ASDCP::TimedText::TimedTextDescriptor descriptor;
1732         descriptor.ContainerDuration = asset->intrinsic_duration();
1733         Kumu::hex2bin (asset->xml_id()->c_str(), descriptor.AssetID, ASDCP::UUIDlen, &c);
1734         DCP_ASSERT (c == Kumu::UUID_Length);
1735         ASDCP::Result_t r = writer.OpenWrite (subs_mxf.string().c_str(), writer_info, descriptor, 16384);
1736         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1737         r = writer.WriteTimedTextResource (dcp::file_to_string(subs_xml));
1738         BOOST_REQUIRE (!ASDCP_FAILURE(r));
1739         writer.Finalize ();
1740
1741         auto reel_asset = make_shared<T>(asset, dcp::Fraction(24, 1), asset->intrinsic_duration(), 0);
1742         return write_dcp_with_single_asset (dir, reel_asset);
1743 }
1744
1745
1746 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_first_text_time)
1747 {
1748         auto const dir = path("build/test/verify_invalid_subtitle_first_text_time");
1749         /* Just too early */
1750         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24 - 1, 5 * 24 }});
1751         check_verify_result (
1752                 { dir },
1753                 {
1754                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1755                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1756                 });
1757
1758 }
1759
1760
1761 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time)
1762 {
1763         auto const dir = path("build/test/verify_valid_subtitle_first_text_time");
1764         /* Just late enough */
1765         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 5 * 24 }});
1766         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1767 }
1768
1769
1770 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_first_text_time_on_second_reel)
1771 {
1772         auto const dir = path("build/test/verify_valid_subtitle_first_text_time_on_second_reel");
1773         prepare_directory (dir);
1774
1775         auto asset1 = make_shared<dcp::SMPTESubtitleAsset>();
1776         asset1->set_start_time (dcp::Time());
1777         /* Just late enough */
1778         add_test_subtitle (asset1, 4 * 24, 5 * 24);
1779         asset1->set_language (dcp::LanguageTag("de-DE"));
1780         asset1->write (dir / "subs1.mxf");
1781         auto reel_asset1 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset1, dcp::Fraction(24, 1), 5 * 24, 0);
1782         auto reel1 = make_shared<dcp::Reel>();
1783         reel1->add (reel_asset1);
1784         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 5 * 24);
1785         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
1786         reel1->add (markers1);
1787
1788         auto asset2 = make_shared<dcp::SMPTESubtitleAsset>();
1789         asset2->set_start_time (dcp::Time());
1790         /* This would be too early on first reel but should be OK on the second */
1791         add_test_subtitle (asset2, 3, 4 * 24);
1792         asset2->set_language (dcp::LanguageTag("de-DE"));
1793         asset2->write (dir / "subs2.mxf");
1794         auto reel_asset2 = make_shared<dcp::ReelSMPTESubtitleAsset>(asset2, dcp::Fraction(24, 1), 4 * 24, 0);
1795         auto reel2 = make_shared<dcp::Reel>();
1796         reel2->add (reel_asset2);
1797         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 4 * 24);
1798         markers2->set (dcp::Marker::LFOC, dcp::Time(4 * 24 - 1, 24, 24));
1799         reel2->add (markers2);
1800
1801         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
1802         cpl->add (reel1);
1803         cpl->add (reel2);
1804         auto dcp = make_shared<dcp::DCP>(dir);
1805         dcp->add (cpl);
1806         dcp->set_annotation_text("hello");
1807         dcp->write_xml();
1808
1809         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1810 }
1811
1812
1813 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_spacing)
1814 {
1815         auto const dir = path("build/test/verify_invalid_subtitle_spacing");
1816         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1817                 dir,
1818                 {
1819                         { 4 * 24,     5 * 24 },
1820                         { 5 * 24 + 1, 6 * 24 },
1821                 });
1822         check_verify_result (
1823                 {dir},
1824                 {
1825                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_SPACING },
1826                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1827                 });
1828 }
1829
1830
1831 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_spacing)
1832 {
1833         auto const dir = path("build/test/verify_valid_subtitle_spacing");
1834         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1835                 dir,
1836                 {
1837                         { 4 * 24,      5 * 24 },
1838                         { 5 * 24 + 16, 8 * 24 },
1839                 });
1840         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1841 }
1842
1843
1844 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_duration)
1845 {
1846         auto const dir = path("build/test/verify_invalid_subtitle_duration");
1847         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 1 }});
1848         check_verify_result (
1849                 {dir},
1850                 {
1851                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_DURATION },
1852                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1853                 });
1854 }
1855
1856
1857 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_duration)
1858 {
1859         auto const dir = path("build/test/verify_valid_subtitle_duration");
1860         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (dir, {{ 4 * 24, 4 * 24 + 17 }});
1861         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1862 }
1863
1864
1865 BOOST_AUTO_TEST_CASE (verify_subtitle_overlapping_reel_boundary)
1866 {
1867         auto const dir = path("build/test/verify_subtitle_overlapping_reel_boundary");
1868         prepare_directory (dir);
1869         auto asset = make_shared<dcp::SMPTESubtitleAsset>();
1870         asset->set_start_time (dcp::Time());
1871         add_test_subtitle (asset, 0, 4 * 24);
1872         asset->set_language (dcp::LanguageTag("de-DE"));
1873         asset->write (dir / "subs.mxf");
1874
1875         auto reel_asset = make_shared<dcp::ReelSMPTESubtitleAsset>(asset, dcp::Fraction(24, 1), 3 * 24, 0);
1876         auto cpl = write_dcp_with_single_asset (dir, reel_asset);
1877         check_verify_result (
1878                 {dir},
1879                 {
1880                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "72 96", boost::filesystem::canonical(asset->file().get()) },
1881                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
1882                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::SUBTITLE_OVERLAPS_REEL_BOUNDARY },
1883                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1884                 });
1885
1886 }
1887
1888
1889 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count1)
1890 {
1891         auto const dir = path ("build/test/invalid_subtitle_line_count1");
1892         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1893                 dir,
1894                 {
1895                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1896                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1897                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1898                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
1899                 });
1900         check_verify_result (
1901                 {dir},
1902                 {
1903                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1904                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1905                 });
1906 }
1907
1908
1909 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count1)
1910 {
1911         auto const dir = path ("build/test/verify_valid_subtitle_line_count1");
1912         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1913                 dir,
1914                 {
1915                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1916                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
1917                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
1918                 });
1919         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1920 }
1921
1922
1923 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_count2)
1924 {
1925         auto const dir = path ("build/test/verify_invalid_subtitle_line_count2");
1926         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1927                 dir,
1928                 {
1929                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1930                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1931                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1932                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
1933                 });
1934         check_verify_result (
1935                 {dir},
1936                 {
1937                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_COUNT },
1938                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1939                 });
1940 }
1941
1942
1943 BOOST_AUTO_TEST_CASE (verify_valid_subtitle_line_count2)
1944 {
1945         auto const dir = path ("build/test/verify_valid_subtitle_line_count2");
1946         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1947                 dir,
1948                 {
1949                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
1950                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
1951                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
1952                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
1953                 });
1954         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
1955 }
1956
1957
1958 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length1)
1959 {
1960         auto const dir = path ("build/test/verify_invalid_subtitle_line_length1");
1961         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1962                 dir,
1963                 {
1964                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123" }
1965                 });
1966         check_verify_result (
1967                 {dir},
1968                 {
1969                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::NEARLY_INVALID_SUBTITLE_LINE_LENGTH },
1970                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1971                 });
1972 }
1973
1974
1975 BOOST_AUTO_TEST_CASE (verify_invalid_subtitle_line_length2)
1976 {
1977         auto const dir = path ("build/test/verify_invalid_subtitle_line_length2");
1978         auto cpl = dcp_with_text<dcp::ReelSMPTESubtitleAsset> (
1979                 dir,
1980                 {
1981                         { 96, 300, 0.0, dcp::VAlign::CENTER, "012345678901234567890123456789012345678901234567890123456789012345678901234567890" }
1982                 });
1983         check_verify_result (
1984                 {dir},
1985                 {
1986                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_LINE_LENGTH },
1987                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
1988                 });
1989 }
1990
1991
1992 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count1)
1993 {
1994         auto const dir = path ("build/test/verify_valid_closed_caption_line_count1");
1995         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
1996                 dir,
1997                 {
1998                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
1999                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
2000                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
2001                         { 96, 200, 0.3, dcp::VAlign::CENTER, "lines" }
2002                 });
2003         check_verify_result (
2004                 {dir},
2005                 {
2006                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
2007                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2008                 });
2009 }
2010
2011
2012 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count2)
2013 {
2014         auto const dir = path ("build/test/verify_valid_closed_caption_line_count2");
2015         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2016                 dir,
2017                 {
2018                         { 96, 200, 0.0, dcp::VAlign::CENTER, "We" },
2019                         { 96, 200, 0.1, dcp::VAlign::CENTER, "have" },
2020                         { 96, 200, 0.2, dcp::VAlign::CENTER, "four" },
2021                 });
2022         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2023 }
2024
2025
2026 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_count3)
2027 {
2028         auto const dir = path ("build/test/verify_invalid_closed_caption_line_count3");
2029         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2030                 dir,
2031                 {
2032                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
2033                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
2034                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
2035                         { 150, 180, 0.3, dcp::VAlign::CENTER, "lines" }
2036                 });
2037         check_verify_result (
2038                 {dir},
2039                 {
2040                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_COUNT},
2041                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2042                 });
2043 }
2044
2045
2046 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_count4)
2047 {
2048         auto const dir = path ("build/test/verify_valid_closed_caption_line_count4");
2049         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2050                 dir,
2051                 {
2052                         { 96, 300, 0.0, dcp::VAlign::CENTER, "We" },
2053                         { 96, 300, 0.1, dcp::VAlign::CENTER, "have" },
2054                         { 150, 180, 0.2, dcp::VAlign::CENTER, "four" },
2055                         { 190, 250, 0.3, dcp::VAlign::CENTER, "lines" }
2056                 });
2057         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2058 }
2059
2060
2061 BOOST_AUTO_TEST_CASE (verify_valid_closed_caption_line_length)
2062 {
2063         auto const dir = path ("build/test/verify_valid_closed_caption_line_length");
2064         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2065                 dir,
2066                 {
2067                         { 96, 300, 0.0, dcp::VAlign::CENTER, "01234567890123456789012345678901" }
2068                 });
2069         check_verify_result (
2070                 {dir},
2071                 {
2072                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2073                 });
2074 }
2075
2076
2077 BOOST_AUTO_TEST_CASE (verify_invalid_closed_caption_line_length)
2078 {
2079         auto const dir = path ("build/test/verify_invalid_closed_caption_line_length");
2080         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2081                 dir,
2082                 {
2083                         { 96, 300, 0.0, dcp::VAlign::CENTER, "0123456789012345678901234567890123" }
2084                 });
2085         check_verify_result (
2086                 {dir},
2087                 {
2088                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_CLOSED_CAPTION_LINE_LENGTH },
2089                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2090                 });
2091 }
2092
2093
2094 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign1)
2095 {
2096         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign1");
2097         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2098                 dir,
2099                 {
2100                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2101                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2102                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2103                 });
2104         check_verify_result (
2105                 {dir},
2106                 {
2107                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2108                 });
2109 }
2110
2111
2112 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_valign2)
2113 {
2114         auto const dir = path ("build/test/verify_mismatched_closed_caption_valign2");
2115         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2116                 dir,
2117                 {
2118                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2119                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2120                         { 96, 300, 0.2, dcp::VAlign::CENTER, "not fine" },
2121                 });
2122         check_verify_result (
2123                 {dir},
2124                 {
2125                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_VALIGN },
2126                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2127                 });
2128 }
2129
2130
2131 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering1)
2132 {
2133         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering1");
2134         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2135                 dir,
2136                 {
2137                         { 96, 300, 0.0, dcp::VAlign::TOP, "This" },
2138                         { 96, 300, 0.1, dcp::VAlign::TOP, "is" },
2139                         { 96, 300, 0.2, dcp::VAlign::TOP, "fine" },
2140                 });
2141         check_verify_result (
2142                 {dir},
2143                 {
2144                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2145                 });
2146 }
2147
2148
2149 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering2)
2150 {
2151         auto const dir = path ("build/test/verify_invalid_incorrect_closed_caption_ordering2");
2152         auto cpl = dcp_with_text<dcp::ReelSMPTEClosedCaptionAsset> (
2153                 dir,
2154                 {
2155                         { 96, 300, 0.2, dcp::VAlign::BOTTOM, "This" },
2156                         { 96, 300, 0.1, dcp::VAlign::BOTTOM, "is" },
2157                         { 96, 300, 0.0, dcp::VAlign::BOTTOM, "also fine" },
2158                 });
2159         check_verify_result (
2160                 {dir},
2161                 {
2162                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2163                 });
2164 }
2165
2166
2167 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering3)
2168 {
2169         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering3");
2170         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering3.xml");
2171         check_verify_result (
2172                 {dir},
2173                 {
2174                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ORDERING },
2175                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2176                 });
2177 }
2178
2179
2180 BOOST_AUTO_TEST_CASE (verify_incorrect_closed_caption_ordering4)
2181 {
2182         auto const dir = path ("build/test/verify_incorrect_closed_caption_ordering4");
2183         auto cpl = dcp_with_text_from_file<dcp::ReelSMPTEClosedCaptionAsset> (dir, "test/data/verify_incorrect_closed_caption_ordering4.xml");
2184         check_verify_result (
2185                 {dir},
2186                 {
2187                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2188                 });
2189 }
2190
2191
2192
2193 BOOST_AUTO_TEST_CASE (verify_invalid_sound_frame_rate)
2194 {
2195         path const dir("build/test/verify_invalid_sound_frame_rate");
2196         prepare_directory (dir);
2197
2198         auto picture = simple_picture (dir, "foo");
2199         auto reel_picture = make_shared<dcp::ReelMonoPictureAsset>(picture, 0);
2200         auto reel = make_shared<dcp::Reel>();
2201         reel->add (reel_picture);
2202         auto sound = simple_sound (dir, "foo", dcp::MXFMetadata(), "de-DE", 24, 96000, boost::none);
2203         auto reel_sound = make_shared<dcp::ReelSoundAsset>(sound, 0);
2204         reel->add (reel_sound);
2205         reel->add (simple_markers());
2206         auto cpl = make_shared<dcp::CPL>("hello", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2207         cpl->add (reel);
2208         auto dcp = make_shared<dcp::DCP>(dir);
2209         dcp->add (cpl);
2210         dcp->set_annotation_text("hello");
2211         dcp->write_xml();
2212
2213         check_verify_result (
2214                 {dir},
2215                 {
2216                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_SOUND_FRAME_RATE, string("96000"), canonical(dir / "audiofoo.mxf") },
2217                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2218                 });
2219 }
2220
2221
2222 BOOST_AUTO_TEST_CASE (verify_missing_cpl_annotation_text)
2223 {
2224         path const dir("build/test/verify_missing_cpl_annotation_text");
2225         auto dcp = make_simple (dir);
2226         dcp->set_annotation_text("A Test DCP");
2227         dcp->write_xml();
2228
2229         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2230
2231         auto const cpl = dcp->cpls()[0];
2232
2233         {
2234                 BOOST_REQUIRE (cpl->file());
2235                 Editor e(cpl->file().get());
2236                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "");
2237         }
2238
2239         check_verify_result (
2240                 {dir},
2241                 {
2242                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2243                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2244                 });
2245 }
2246
2247
2248 BOOST_AUTO_TEST_CASE (verify_mismatched_cpl_annotation_text)
2249 {
2250         path const dir("build/test/verify_mismatched_cpl_annotation_text");
2251         auto dcp = make_simple (dir);
2252         dcp->set_annotation_text("A Test DCP");
2253         dcp->write_xml();
2254
2255         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2256         auto const cpl = dcp->cpls()[0];
2257
2258         {
2259                 BOOST_REQUIRE (cpl->file());
2260                 Editor e(cpl->file().get());
2261                 e.replace("<AnnotationText>A Test DCP</AnnotationText>", "<AnnotationText>A Test DCP 1</AnnotationText>");
2262         }
2263
2264         check_verify_result (
2265                 {dir},
2266                 {
2267                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISMATCHED_CPL_ANNOTATION_TEXT, cpl->id(), canonical(cpl->file().get()) },
2268                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), canonical(cpl->file().get()) }
2269                 });
2270 }
2271
2272
2273 BOOST_AUTO_TEST_CASE (verify_mismatched_asset_duration)
2274 {
2275         path const dir("build/test/verify_mismatched_asset_duration");
2276         prepare_directory (dir);
2277         shared_ptr<dcp::DCP> dcp (new dcp::DCP(dir));
2278         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2279
2280         shared_ptr<dcp::MonoPictureAsset> mp = simple_picture (dir, "", 24);
2281         shared_ptr<dcp::SoundAsset> ms = simple_sound (dir, "", dcp::MXFMetadata(), "en-US", 25);
2282
2283         auto reel = make_shared<dcp::Reel>(
2284                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
2285                 make_shared<dcp::ReelSoundAsset>(ms, 0)
2286                 );
2287
2288         reel->add (simple_markers());
2289         cpl->add (reel);
2290
2291         dcp->add (cpl);
2292         dcp->set_annotation_text("A Test DCP");
2293         dcp->write_xml();
2294
2295         check_verify_result (
2296                 {dir},
2297                 {
2298                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_ASSET_DURATION },
2299                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), canonical(cpl->file().get()) }
2300                 });
2301 }
2302
2303
2304
2305 static
2306 shared_ptr<dcp::CPL>
2307 verify_subtitles_must_be_in_all_reels_check (path dir, bool add_to_reel1, bool add_to_reel2)
2308 {
2309         prepare_directory (dir);
2310         auto dcp = make_shared<dcp::DCP>(dir);
2311         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2312
2313         auto constexpr reel_length = 192;
2314
2315         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2316         subs->set_language (dcp::LanguageTag("de-DE"));
2317         subs->set_start_time (dcp::Time());
2318         subs->add (simple_subtitle());
2319         subs->write (dir / "subs.mxf");
2320         auto reel_subs = make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0);
2321
2322         auto reel1 = make_shared<dcp::Reel>(
2323                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2324                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2325                 );
2326
2327         if (add_to_reel1) {
2328                 reel1->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2329         }
2330
2331         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2332         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2333         reel1->add (markers1);
2334
2335         cpl->add (reel1);
2336
2337         auto reel2 = make_shared<dcp::Reel>(
2338                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2339                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2340                 );
2341
2342         if (add_to_reel2) {
2343                 reel2->add (make_shared<dcp::ReelSMPTESubtitleAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2344         }
2345
2346         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2347         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2348         reel2->add (markers2);
2349
2350         cpl->add (reel2);
2351
2352         dcp->add (cpl);
2353         dcp->set_annotation_text("A Test DCP");
2354         dcp->write_xml();
2355
2356         return cpl;
2357 }
2358
2359
2360 BOOST_AUTO_TEST_CASE (verify_missing_main_subtitle_from_some_reels)
2361 {
2362         {
2363                 path dir ("build/test/missing_main_subtitle_from_some_reels");
2364                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, false);
2365                 check_verify_result (
2366                         { dir },
2367                         {
2368                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_MAIN_SUBTITLE_FROM_SOME_REELS },
2369                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2370                         });
2371
2372         }
2373
2374         {
2375                 path dir ("build/test/verify_subtitles_must_be_in_all_reels2");
2376                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, true, true);
2377                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2378         }
2379
2380         {
2381                 path dir ("build/test/verify_subtitles_must_be_in_all_reels1");
2382                 auto cpl = verify_subtitles_must_be_in_all_reels_check (dir, false, false);
2383                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2384         }
2385 }
2386
2387
2388 static
2389 shared_ptr<dcp::CPL>
2390 verify_closed_captions_must_be_in_all_reels_check (path dir, int caps_in_reel1, int caps_in_reel2)
2391 {
2392         prepare_directory (dir);
2393         auto dcp = make_shared<dcp::DCP>(dir);
2394         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2395
2396         auto constexpr reel_length = 192;
2397
2398         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2399         subs->set_language (dcp::LanguageTag("de-DE"));
2400         subs->set_start_time (dcp::Time());
2401         subs->add (simple_subtitle());
2402         subs->write (dir / "subs.mxf");
2403
2404         auto reel1 = make_shared<dcp::Reel>(
2405                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2406                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2407                 );
2408
2409         for (int i = 0; i < caps_in_reel1; ++i) {
2410                 reel1->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2411         }
2412
2413         auto markers1 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2414         markers1->set (dcp::Marker::FFOC, dcp::Time(1, 24, 24));
2415         reel1->add (markers1);
2416
2417         cpl->add (reel1);
2418
2419         auto reel2 = make_shared<dcp::Reel>(
2420                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2421                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2422                 );
2423
2424         for (int i = 0; i < caps_in_reel2; ++i) {
2425                 reel2->add (make_shared<dcp::ReelSMPTEClosedCaptionAsset>(subs, dcp::Fraction(24, 1), reel_length, 0));
2426         }
2427
2428         auto markers2 = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), reel_length);
2429         markers2->set (dcp::Marker::LFOC, dcp::Time(reel_length - 1, 24, 24));
2430         reel2->add (markers2);
2431
2432         cpl->add (reel2);
2433
2434         dcp->add (cpl);
2435         dcp->set_annotation_text("A Test DCP");
2436         dcp->write_xml();
2437
2438         return cpl;
2439 }
2440
2441
2442 BOOST_AUTO_TEST_CASE (verify_mismatched_closed_caption_asset_counts)
2443 {
2444         {
2445                 path dir ("build/test/mismatched_closed_caption_asset_counts");
2446                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 3, 4);
2447                 check_verify_result (
2448                         {dir},
2449                         {
2450                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_CLOSED_CAPTION_ASSET_COUNTS },
2451                                 { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
2452                         });
2453         }
2454
2455         {
2456                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels2");
2457                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 4, 4);
2458                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2459         }
2460
2461         {
2462                 path dir ("build/test/verify_closed_captions_must_be_in_all_reels3");
2463                 auto cpl = verify_closed_captions_must_be_in_all_reels_check (dir, 0, 0);
2464                 check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }});
2465         }
2466 }
2467
2468
2469 template <class T>
2470 void
2471 verify_text_entry_point_check (path dir, dcp::VerificationNote::Code code, boost::function<void (shared_ptr<T>)> adjust)
2472 {
2473         prepare_directory (dir);
2474         auto dcp = make_shared<dcp::DCP>(dir);
2475         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
2476
2477         auto constexpr reel_length = 192;
2478
2479         auto subs = make_shared<dcp::SMPTESubtitleAsset>();
2480         subs->set_language (dcp::LanguageTag("de-DE"));
2481         subs->set_start_time (dcp::Time());
2482         subs->add (simple_subtitle());
2483         subs->write (dir / "subs.mxf");
2484         auto reel_text = make_shared<T>(subs, dcp::Fraction(24, 1), reel_length, 0);
2485         adjust (reel_text);
2486
2487         auto reel = make_shared<dcp::Reel>(
2488                 make_shared<dcp::ReelMonoPictureAsset>(simple_picture(dir, "", reel_length), 0),
2489                 make_shared<dcp::ReelSoundAsset>(simple_sound(dir, "", dcp::MXFMetadata(), "en-US", reel_length), 0)
2490                 );
2491
2492         reel->add (reel_text);
2493
2494         reel->add (simple_markers(reel_length));
2495
2496         cpl->add (reel);
2497
2498         dcp->add (cpl);
2499         dcp->set_annotation_text("A Test DCP");
2500         dcp->write_xml();
2501
2502         check_verify_result (
2503                 {dir},
2504                 {
2505                         { dcp::VerificationNote::Type::BV21_ERROR, code, subs->id() },
2506                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
2507                 });
2508 }
2509
2510
2511 BOOST_AUTO_TEST_CASE (verify_text_entry_point)
2512 {
2513         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2514                 "build/test/verify_subtitle_entry_point_must_be_present",
2515                 dcp::VerificationNote::Code::MISSING_SUBTITLE_ENTRY_POINT,
2516                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2517                         asset->unset_entry_point ();
2518                         }
2519                 );
2520
2521         verify_text_entry_point_check<dcp::ReelSMPTESubtitleAsset> (
2522                 "build/test/verify_subtitle_entry_point_must_be_zero",
2523                 dcp::VerificationNote::Code::INCORRECT_SUBTITLE_ENTRY_POINT,
2524                 [](shared_ptr<dcp::ReelSMPTESubtitleAsset> asset) {
2525                         asset->set_entry_point (4);
2526                         }
2527                 );
2528
2529         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2530                 "build/test/verify_closed_caption_entry_point_must_be_present",
2531                 dcp::VerificationNote::Code::MISSING_CLOSED_CAPTION_ENTRY_POINT,
2532                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2533                         asset->unset_entry_point ();
2534                         }
2535                 );
2536
2537         verify_text_entry_point_check<dcp::ReelSMPTEClosedCaptionAsset> (
2538                 "build/test/verify_closed_caption_entry_point_must_be_zero",
2539                 dcp::VerificationNote::Code::INCORRECT_CLOSED_CAPTION_ENTRY_POINT,
2540                 [](shared_ptr<dcp::ReelSMPTEClosedCaptionAsset> asset) {
2541                         asset->set_entry_point (9);
2542                         }
2543                 );
2544 }
2545
2546
2547 BOOST_AUTO_TEST_CASE (verify_missing_hash)
2548 {
2549         RNGFixer fix;
2550
2551         path const dir("build/test/verify_missing_hash");
2552         auto dcp = make_simple (dir);
2553         dcp->set_annotation_text("A Test DCP");
2554         dcp->write_xml();
2555
2556         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2557         auto const cpl = dcp->cpls()[0];
2558         BOOST_REQUIRE_EQUAL (cpl->reels().size(), 1U);
2559         BOOST_REQUIRE (cpl->reels()[0]->main_picture());
2560         auto asset_id = cpl->reels()[0]->main_picture()->id();
2561
2562         {
2563                 BOOST_REQUIRE (cpl->file());
2564                 Editor e(cpl->file().get());
2565                 e.delete_first_line_containing("<Hash>");
2566         }
2567
2568         check_verify_result (
2569                 {dir},
2570                 {
2571                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2572                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_HASH, asset_id }
2573                 });
2574 }
2575
2576
2577 static
2578 void
2579 verify_markers_test (
2580         path dir,
2581         vector<pair<dcp::Marker, dcp::Time>> markers,
2582         vector<dcp::VerificationNote> test_notes
2583         )
2584 {
2585         auto dcp = make_simple (dir);
2586         dcp->cpls()[0]->set_content_kind (dcp::ContentKind::FEATURE);
2587         auto markers_asset = make_shared<dcp::ReelMarkersAsset>(dcp::Fraction(24, 1), 24);
2588         for (auto const& i: markers) {
2589                 markers_asset->set (i.first, i.second);
2590         }
2591         dcp->cpls()[0]->reels()[0]->add(markers_asset);
2592         dcp->set_annotation_text("A Test DCP");
2593         dcp->write_xml();
2594
2595         check_verify_result ({dir}, test_notes);
2596 }
2597
2598
2599 BOOST_AUTO_TEST_CASE (verify_markers)
2600 {
2601         verify_markers_test (
2602                 "build/test/verify_markers_all_correct",
2603                 {
2604                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2605                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2606                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2607                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2608                 },
2609                 {}
2610                 );
2611
2612         verify_markers_test (
2613                 "build/test/verify_markers_missing_ffec",
2614                 {
2615                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2616                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2617                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2618                 },
2619                 {
2620                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE }
2621                 });
2622
2623         verify_markers_test (
2624                 "build/test/verify_markers_missing_ffmc",
2625                 {
2626                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2627                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2628                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2629                 },
2630                 {
2631                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE }
2632                 });
2633
2634         verify_markers_test (
2635                 "build/test/verify_markers_missing_ffoc",
2636                 {
2637                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2638                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2639                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2640                 },
2641                 {
2642                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC}
2643                 });
2644
2645         verify_markers_test (
2646                 "build/test/verify_markers_missing_lfoc",
2647                 {
2648                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2649                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2650                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) }
2651                 },
2652                 {
2653                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC }
2654                 });
2655
2656         verify_markers_test (
2657                 "build/test/verify_markers_incorrect_ffoc",
2658                 {
2659                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2660                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2661                         { dcp::Marker::FFOC, dcp::Time(3, 24, 24) },
2662                         { dcp::Marker::LFOC, dcp::Time(23, 24, 24) }
2663                 },
2664                 {
2665                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_FFOC, string("3") }
2666                 });
2667
2668         verify_markers_test (
2669                 "build/test/verify_markers_incorrect_lfoc",
2670                 {
2671                         { dcp::Marker::FFEC, dcp::Time(12, 24, 24) },
2672                         { dcp::Marker::FFMC, dcp::Time(13, 24, 24) },
2673                         { dcp::Marker::FFOC, dcp::Time(1, 24, 24) },
2674                         { dcp::Marker::LFOC, dcp::Time(18, 24, 24) }
2675                 },
2676                 {
2677                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INCORRECT_LFOC, string("18") }
2678                 });
2679 }
2680
2681
2682 BOOST_AUTO_TEST_CASE (verify_missing_cpl_metadata_version_number)
2683 {
2684         path dir = "build/test/verify_missing_cpl_metadata_version_number";
2685         prepare_directory (dir);
2686         auto dcp = make_simple (dir);
2687         auto cpl = dcp->cpls()[0];
2688         cpl->unset_version_number();
2689         dcp->set_annotation_text("A Test DCP");
2690         dcp->write_xml();
2691
2692         check_verify_result ({dir}, {{ dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA_VERSION_NUMBER, cpl->id(), cpl->file().get() }});
2693 }
2694
2695
2696 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata1)
2697 {
2698         path dir = "build/test/verify_missing_extension_metadata1";
2699         auto dcp = make_simple (dir);
2700         dcp->set_annotation_text("A Test DCP");
2701         dcp->write_xml();
2702
2703         BOOST_REQUIRE_EQUAL (dcp->cpls().size(), 1U);
2704         auto cpl = dcp->cpls()[0];
2705
2706         {
2707                 Editor e (cpl->file().get());
2708                 e.delete_lines ("<meta:ExtensionMetadataList>", "</meta:ExtensionMetadataList>");
2709         }
2710
2711         check_verify_result (
2712                 {dir},
2713                 {
2714                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2715                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2716                 });
2717 }
2718
2719
2720 BOOST_AUTO_TEST_CASE (verify_missing_extension_metadata2)
2721 {
2722         path dir = "build/test/verify_missing_extension_metadata2";
2723         auto dcp = make_simple (dir);
2724         dcp->set_annotation_text("A Test DCP");
2725         dcp->write_xml();
2726
2727         auto cpl = dcp->cpls()[0];
2728
2729         {
2730                 Editor e (cpl->file().get());
2731                 e.delete_lines ("<meta:ExtensionMetadata scope=\"http://isdcf.com/ns/cplmd/app\">", "</meta:ExtensionMetadata>");
2732         }
2733
2734         check_verify_result (
2735                 {dir},
2736                 {
2737                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2738                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_EXTENSION_METADATA, cpl->id(), cpl->file().get() }
2739                 });
2740 }
2741
2742
2743 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata3)
2744 {
2745         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata3";
2746         auto dcp = make_simple (dir);
2747         dcp->set_annotation_text("A Test DCP");
2748         dcp->write_xml();
2749
2750         auto const cpl = dcp->cpls()[0];
2751
2752         {
2753                 Editor e (cpl->file().get());
2754                 e.replace ("<meta:Name>A", "<meta:NameX>A");
2755                 e.replace ("n</meta:Name>", "n</meta:NameX>");
2756         }
2757
2758         check_verify_result (
2759                 {dir},
2760                 {
2761                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:NameX'"), cpl->file().get(), 70 },
2762                         { 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 },
2763                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2764                 });
2765 }
2766
2767
2768 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata1)
2769 {
2770         path dir = "build/test/verify_invalid_extension_metadata1";
2771         auto dcp = make_simple (dir);
2772         dcp->set_annotation_text("A Test DCP");
2773         dcp->write_xml();
2774
2775         auto cpl = dcp->cpls()[0];
2776
2777         {
2778                 Editor e (cpl->file().get());
2779                 e.replace ("Application", "Fred");
2780         }
2781
2782         check_verify_result (
2783                 {dir},
2784                 {
2785                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2786                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> should be 'Application'"), cpl->file().get() },
2787                 });
2788 }
2789
2790
2791 BOOST_AUTO_TEST_CASE (verify_invalid_extension_metadata2)
2792 {
2793         path dir = "build/test/verify_invalid_extension_metadata2";
2794         auto dcp = make_simple (dir);
2795         dcp->set_annotation_text("A Test DCP");
2796         dcp->write_xml();
2797
2798         auto cpl = dcp->cpls()[0];
2799
2800         {
2801                 Editor e (cpl->file().get());
2802                 e.replace ("DCP Constraints Profile", "Fred");
2803         }
2804
2805         check_verify_result (
2806                 {dir},
2807                 {
2808                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2809                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INVALID_EXTENSION_METADATA, string("<Name> property should be 'DCP Constraints Profile'"), cpl->file().get() },
2810                 });
2811 }
2812
2813
2814 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata6)
2815 {
2816         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata6";
2817         auto dcp = make_simple (dir);
2818         dcp->set_annotation_text("A Test DCP");
2819         dcp->write_xml();
2820
2821         auto const cpl = dcp->cpls()[0];
2822
2823         {
2824                 Editor e (cpl->file().get());
2825                 e.replace ("<meta:Value>", "<meta:ValueX>");
2826                 e.replace ("</meta:Value>", "</meta:ValueX>");
2827         }
2828
2829         check_verify_result (
2830                 {dir},
2831                 {
2832                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:ValueX'"), cpl->file().get(), 74 },
2833                         { 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 },
2834                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2835                 });
2836 }
2837
2838
2839 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata7)
2840 {
2841         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata7";
2842         auto dcp = make_simple (dir);
2843         dcp->set_annotation_text("A Test DCP");
2844         dcp->write_xml();
2845
2846         auto const cpl = dcp->cpls()[0];
2847
2848         {
2849                 Editor e (cpl->file().get());
2850                 e.replace ("SMPTE-RDD-52:2020-Bv2.1", "Fred");
2851         }
2852
2853         check_verify_result (
2854                 {dir},
2855                 {
2856                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2857                         { 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() },
2858                 });
2859 }
2860
2861
2862 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata8)
2863 {
2864         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata8";
2865         auto dcp = make_simple (dir);
2866         dcp->set_annotation_text("A Test DCP");
2867         dcp->write_xml();
2868
2869         auto const cpl = dcp->cpls()[0];
2870
2871         {
2872                 Editor e (cpl->file().get());
2873                 e.replace ("<meta:Property>", "<meta:PropertyX>");
2874                 e.replace ("</meta:Property>", "</meta:PropertyX>");
2875         }
2876
2877         check_verify_result (
2878                 {dir},
2879                 {
2880                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyX'"), cpl->file().get(), 72 },
2881                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("element 'meta:PropertyX' is not allowed for content model '(Property+)'"), cpl->file().get(), 76 },
2882                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2883                 });
2884 }
2885
2886
2887 BOOST_AUTO_TEST_CASE (verify_invalid_xml_cpl_extension_metadata9)
2888 {
2889         path dir = "build/test/verify_invalid_xml_cpl_extension_metadata9";
2890         auto dcp = make_simple (dir);
2891         dcp->set_annotation_text("A Test DCP");
2892         dcp->write_xml();
2893
2894         auto const cpl = dcp->cpls()[0];
2895
2896         {
2897                 Editor e (cpl->file().get());
2898                 e.replace ("<meta:PropertyList>", "<meta:PropertyListX>");
2899                 e.replace ("</meta:PropertyList>", "</meta:PropertyListX>");
2900         }
2901
2902         check_verify_result (
2903                 {dir},
2904                 {
2905                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_XML, string("no declaration found for element 'meta:PropertyListX'"), cpl->file().get(), 71 },
2906                         { 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 },
2907                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl->id(), cpl->file().get() },
2908                 });
2909 }
2910
2911
2912
2913 BOOST_AUTO_TEST_CASE (verify_unsigned_cpl_with_encrypted_content)
2914 {
2915         path dir = "build/test/verify_unsigned_cpl_with_encrypted_content";
2916         prepare_directory (dir);
2917         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2918                 copy_file (i.path(), dir / i.path().filename());
2919         }
2920
2921         path const pkl = dir / ( "pkl_" + encryption_test_pkl_id + ".xml" );
2922         path const cpl = dir / ( "cpl_" + encryption_test_cpl_id + ".xml");
2923
2924         {
2925                 Editor e (cpl);
2926                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2927         }
2928
2929         check_verify_result (
2930                 {dir},
2931                 {
2932                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, encryption_test_cpl_id, canonical(cpl) },
2933                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl), },
2934                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2935                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2936                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2937                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2938                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2939                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_CPL_WITH_ENCRYPTED_CONTENT, encryption_test_cpl_id, canonical(cpl) }
2940                 });
2941 }
2942
2943
2944 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_encrypted_content)
2945 {
2946         path dir = "build/test/unsigned_pkl_with_encrypted_content";
2947         prepare_directory (dir);
2948         for (auto i: directory_iterator("test/ref/DCP/encryption_test")) {
2949                 copy_file (i.path(), dir / i.path().filename());
2950         }
2951
2952         path const cpl = dir / ("cpl_" + encryption_test_cpl_id + ".xml");
2953         path const pkl = dir / ("pkl_" + encryption_test_pkl_id + ".xml");
2954         {
2955                 Editor e (pkl);
2956                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2957         }
2958
2959         check_verify_result (
2960                 {dir},
2961                 {
2962                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, encryption_test_pkl_id, canonical(pkl) },
2963                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFEC_IN_FEATURE },
2964                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_FFMC_IN_FEATURE },
2965                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_FFOC },
2966                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::MISSING_LFOC },
2967                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, encryption_test_cpl_id, canonical(cpl) },
2968                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::UNSIGNED_PKL_WITH_ENCRYPTED_CONTENT, encryption_test_pkl_id, canonical(pkl) },
2969                 });
2970 }
2971
2972
2973 BOOST_AUTO_TEST_CASE (verify_unsigned_pkl_with_unencrypted_content)
2974 {
2975         path dir = "build/test/verify_unsigned_pkl_with_unencrypted_content";
2976         prepare_directory (dir);
2977         for (auto i: directory_iterator("test/ref/DCP/dcp_test1")) {
2978                 copy_file (i.path(), dir / i.path().filename());
2979         }
2980
2981         {
2982                 Editor e (dir / dcp_test1_pkl);
2983                 e.delete_lines ("<dsig:Signature", "</dsig:Signature>");
2984         }
2985
2986         check_verify_result ({dir}, {});
2987 }
2988
2989
2990 BOOST_AUTO_TEST_CASE (verify_partially_encrypted)
2991 {
2992         path dir ("build/test/verify_must_not_be_partially_encrypted");
2993         prepare_directory (dir);
2994
2995         dcp::DCP d (dir);
2996
2997         auto signer = make_shared<dcp::CertificateChain>();
2998         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/ca.self-signed.pem")));
2999         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/intermediate.signed.pem")));
3000         signer->add (dcp::Certificate(dcp::file_to_string("test/ref/crypt/leaf.signed.pem")));
3001         signer->set_key (dcp::file_to_string("test/ref/crypt/leaf.key"));
3002
3003         auto cpl = make_shared<dcp::CPL>("A Test DCP", dcp::ContentKind::TRAILER, dcp::Standard::SMPTE);
3004
3005         dcp::Key key;
3006
3007         auto mp = make_shared<dcp::MonoPictureAsset>(dcp::Fraction (24, 1), dcp::Standard::SMPTE);
3008         mp->set_key (key);
3009
3010         auto writer = mp->start_write (dir / "video.mxf", false);
3011         dcp::ArrayData j2c ("test/data/flat_red.j2c");
3012         for (int i = 0; i < 24; ++i) {
3013                 writer->write (j2c.data(), j2c.size());
3014         }
3015         writer->finalize ();
3016
3017         auto ms = simple_sound (dir, "", dcp::MXFMetadata(), "de-DE");
3018
3019         auto reel = make_shared<dcp::Reel>(
3020                 make_shared<dcp::ReelMonoPictureAsset>(mp, 0),
3021                 make_shared<dcp::ReelSoundAsset>(ms, 0)
3022                 );
3023
3024         reel->add (simple_markers());
3025
3026         cpl->add (reel);
3027
3028         cpl->set_content_version (
3029                 {"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"}
3030                 );
3031         cpl->set_annotation_text ("A Test DCP");
3032         cpl->set_issuer ("OpenDCP 0.0.25");
3033         cpl->set_creator ("OpenDCP 0.0.25");
3034         cpl->set_issue_date ("2012-07-17T04:45:18+00:00");
3035         cpl->set_main_sound_configuration ("L,C,R,Lfe,-,-");
3036         cpl->set_main_sound_sample_rate (48000);
3037         cpl->set_main_picture_stored_area (dcp::Size(1998, 1080));
3038         cpl->set_main_picture_active_area (dcp::Size(1440, 1080));
3039         cpl->set_version_number (1);
3040
3041         d.add (cpl);
3042
3043         d.set_issuer("OpenDCP 0.0.25");
3044         d.set_creator("OpenDCP 0.0.25");
3045         d.set_issue_date("2012-07-17T04:45:18+00:00");
3046         d.set_annotation_text("A Test DCP");
3047         d.write_xml(signer);
3048
3049         check_verify_result (
3050                 {dir},
3051                 {
3052                         {dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::PARTIALLY_ENCRYPTED},
3053                 });
3054 }
3055
3056
3057 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_2k)
3058 {
3059         vector<dcp::VerificationNote> notes;
3060         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"));
3061         auto reader = picture.start_read ();
3062         auto frame = reader->get_frame (0);
3063         verify_j2k (frame, notes);
3064         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3065 }
3066
3067
3068 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_4k)
3069 {
3070         vector<dcp::VerificationNote> notes;
3071         dcp::MonoPictureAsset picture (find_file(private_test / "data" / "sul", "TLR"));
3072         auto reader = picture.start_read ();
3073         auto frame = reader->get_frame (0);
3074         verify_j2k (frame, notes);
3075         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3076 }
3077
3078
3079 BOOST_AUTO_TEST_CASE (verify_jpeg2000_codestream_libdcp)
3080 {
3081         boost::filesystem::path dir = "build/test/verify_jpeg2000_codestream_libdcp";
3082         prepare_directory (dir);
3083         auto dcp = make_simple (dir);
3084         dcp->write_xml ();
3085         vector<dcp::VerificationNote> notes;
3086         dcp::MonoPictureAsset picture (find_file(dir, "video"));
3087         auto reader = picture.start_read ();
3088         auto frame = reader->get_frame (0);
3089         verify_j2k (frame, notes);
3090         BOOST_REQUIRE_EQUAL (notes.size(), 0U);
3091 }
3092
3093
3094 /** Check that ResourceID and the XML ID being different is spotted */
3095 BOOST_AUTO_TEST_CASE (verify_mismatched_subtitle_resource_id)
3096 {
3097         boost::filesystem::path const dir = "build/test/verify_mismatched_subtitle_resource_id";
3098         prepare_directory (dir);
3099
3100         ASDCP::WriterInfo writer_info;
3101         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3102
3103         unsigned int c;
3104         auto mxf_id = dcp::make_uuid ();
3105         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3106         BOOST_REQUIRE (c == Kumu::UUID_Length);
3107
3108         auto resource_id = dcp::make_uuid ();
3109         ASDCP::TimedText::TimedTextDescriptor descriptor;
3110         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3111         DCP_ASSERT (c == Kumu::UUID_Length);
3112
3113         auto xml_id = dcp::make_uuid ();
3114         ASDCP::TimedText::MXFWriter writer;
3115         auto subs_mxf = dir / "subs.mxf";
3116         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3117         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3118         writer.WriteTimedTextResource (dcp::String::compose(
3119                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3120                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3121                 "<Id>urn:uuid:%1</Id>"
3122                 "<ContentTitleText>Content</ContentTitleText>"
3123                 "<AnnotationText>Annotation</AnnotationText>"
3124                 "<IssueDate>2018-10-02T12:25:14</IssueDate>"
3125                 "<ReelNumber>1</ReelNumber>"
3126                 "<Language>en-US</Language>"
3127                 "<EditRate>25 1</EditRate>"
3128                 "<TimeCodeRate>25</TimeCodeRate>"
3129                 "<StartTime>00:00:00:00</StartTime>"
3130                 "<SubtitleList>"
3131                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3132                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3133                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3134                 "</Subtitle>"
3135                 "</Font>"
3136                 "</SubtitleList>"
3137                 "</SubtitleReel>",
3138                 xml_id).c_str());
3139
3140         writer.Finalize();
3141
3142         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3143         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3144
3145         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3146
3147         check_verify_result (
3148                 { dir },
3149                 {
3150                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3151                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_RESOURCE_ID },
3152                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3153                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() }
3154                 });
3155 }
3156
3157
3158 /** Check that ResourceID and the MXF ID being the same is spotted */
3159 BOOST_AUTO_TEST_CASE (verify_incorrect_timed_text_id)
3160 {
3161         boost::filesystem::path const dir = "build/test/verify_incorrect_timed_text_id";
3162         prepare_directory (dir);
3163
3164         ASDCP::WriterInfo writer_info;
3165         writer_info.LabelSetType = ASDCP::LS_MXF_SMPTE;
3166
3167         unsigned int c;
3168         auto mxf_id = dcp::make_uuid ();
3169         Kumu::hex2bin (mxf_id.c_str(), writer_info.AssetUUID, Kumu::UUID_Length, &c);
3170         BOOST_REQUIRE (c == Kumu::UUID_Length);
3171
3172         auto resource_id = mxf_id;
3173         ASDCP::TimedText::TimedTextDescriptor descriptor;
3174         Kumu::hex2bin (resource_id.c_str(), descriptor.AssetID, Kumu::UUID_Length, &c);
3175         DCP_ASSERT (c == Kumu::UUID_Length);
3176
3177         auto xml_id = resource_id;
3178         ASDCP::TimedText::MXFWriter writer;
3179         auto subs_mxf = dir / "subs.mxf";
3180         auto r = writer.OpenWrite(subs_mxf.string().c_str(), writer_info, descriptor, 4096);
3181         BOOST_REQUIRE (ASDCP_SUCCESS(r));
3182         writer.WriteTimedTextResource (dcp::String::compose(
3183                 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
3184                 "<SubtitleReel xmlns=\"http://www.smpte-ra.org/schemas/428-7/2010/DCST\" xmlns:xs=\"http://www.w3.org/2001/schema\">"
3185                 "<Id>urn:uuid:%1</Id>"
3186                 "<ContentTitleText>Content</ContentTitleText>"
3187                 "<AnnotationText>Annotation</AnnotationText>"
3188                 "<IssueDate>2018-10-02T12:25:14+02:00</IssueDate>"
3189                 "<ReelNumber>1</ReelNumber>"
3190                 "<Language>en-US</Language>"
3191                 "<EditRate>25 1</EditRate>"
3192                 "<TimeCodeRate>25</TimeCodeRate>"
3193                 "<StartTime>00:00:00:00</StartTime>"
3194                 "<SubtitleList>"
3195                 "<Font ID=\"arial\" Color=\"FFFEFEFE\" Weight=\"normal\" Size=\"42\" Effect=\"border\" EffectColor=\"FF181818\" AspectAdjust=\"1.00\">"
3196                 "<Subtitle SpotNumber=\"1\" TimeIn=\"00:00:03:00\" TimeOut=\"00:00:04:10\" FadeUpTime=\"00:00:00:00\" FadeDownTime=\"00:00:00:00\">"
3197                 "<Text Hposition=\"0.0\" Halign=\"center\" Valign=\"bottom\" Vposition=\"13.5\" Direction=\"ltr\">Hello world</Text>"
3198                 "</Subtitle>"
3199                 "</Font>"
3200                 "</SubtitleList>"
3201                 "</SubtitleReel>",
3202                 xml_id).c_str());
3203
3204         writer.Finalize();
3205
3206         auto subs_asset = make_shared<dcp::SMPTESubtitleAsset>(subs_mxf);
3207         auto subs_reel = make_shared<dcp::ReelSMPTESubtitleAsset>(subs_asset, dcp::Fraction(24, 1), 240, 0);
3208
3209         auto cpl = write_dcp_with_single_asset (dir, subs_reel);
3210
3211         check_verify_result (
3212                 { dir },
3213                 {
3214                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_TIMED_TEXT_DURATION , "240 0", boost::filesystem::canonical(subs_mxf) },
3215                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::INCORRECT_TIMED_TEXT_ASSET_ID },
3216                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_FIRST_TEXT_TIME },
3217                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISSING_CPL_METADATA, cpl->id(), cpl->file().get() },
3218                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::INVALID_SUBTITLE_ISSUE_DATE, string{"2018-10-02T12:25:14+02:00"} }
3219                 });
3220 }
3221
3222
3223 /** Check a DCP with a 3D asset marked as 2D */
3224 BOOST_AUTO_TEST_CASE (verify_threed_marked_as_twod)
3225 {
3226         check_verify_result (
3227                 { private_test / "data" / "xm" },
3228                 {
3229                         {
3230                                 dcp::VerificationNote::Type::WARNING,
3231                                 dcp::VerificationNote::Code::THREED_ASSET_MARKED_AS_TWOD, boost::filesystem::canonical(find_file(private_test / "data" / "xm", "j2c"))
3232                         },
3233                         {
3234                                 dcp::VerificationNote::Type::BV21_ERROR,
3235                                 dcp::VerificationNote::Code::INVALID_STANDARD
3236                         },
3237                 });
3238
3239 }
3240
3241
3242 BOOST_AUTO_TEST_CASE (verify_unexpected_things_in_main_markers)
3243 {
3244         path dir = "build/test/verify_unexpected_things_in_main_markers";
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.insert(
3253                         "          <IntrinsicDuration>24</IntrinsicDuration>",
3254                         "<EntryPoint>0</EntryPoint><Duration>24</Duration>"
3255                         );
3256         }
3257
3258         dcp::CPL cpl (find_cpl(dir));
3259
3260         check_verify_result (
3261                 { dir },
3262                 {
3263                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3264                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_ENTRY_POINT },
3265                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::UNEXPECTED_DURATION },
3266                 });
3267 }
3268
3269
3270 BOOST_AUTO_TEST_CASE(verify_invalid_content_kind)
3271 {
3272         path dir = "build/test/verify_invalid_content_kind";
3273         prepare_directory (dir);
3274         auto dcp = make_simple (dir, 1, 24);
3275         dcp->set_annotation_text("A Test DCP");
3276         dcp->write_xml();
3277
3278         {
3279                 Editor e(find_cpl(dir));
3280                 e.replace("trailer", "trip");
3281         }
3282
3283         dcp::CPL cpl (find_cpl(dir));
3284
3285         check_verify_result (
3286                 { dir },
3287                 {
3288                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3289                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_CONTENT_KIND, string("trip") }
3290                 });
3291
3292 }
3293
3294
3295 BOOST_AUTO_TEST_CASE(verify_valid_content_kind)
3296 {
3297         path dir = "build/test/verify_valid_content_kind";
3298         prepare_directory (dir);
3299         auto dcp = make_simple (dir, 1, 24);
3300         dcp->set_annotation_text("A Test DCP");
3301         dcp->write_xml();
3302
3303         {
3304                 Editor e(find_cpl(dir));
3305                 e.replace("<ContentKind>trailer</ContentKind>", "<ContentKind scope=\"http://bobs.contents/\">trip</ContentKind>");
3306         }
3307
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                 });
3315
3316 }
3317
3318
3319 BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_1)
3320 {
3321         path dir = "build/test/verify_invalid_main_picture_active_area_1";
3322         prepare_directory(dir);
3323         auto dcp = make_simple(dir, 1, 24);
3324         dcp->write_xml();
3325
3326         auto constexpr area = "<meta:MainPictureActiveArea>";
3327
3328         {
3329                 Editor e(find_cpl(dir));
3330                 e.delete_lines_after(area, 2);
3331                 e.insert(area, "<meta:Height>4080</meta:Height>");
3332                 e.insert(area, "<meta:Width>1997</meta:Width>");
3333         }
3334
3335         dcp::PKL pkl(find_pkl(dir));
3336         dcp::CPL cpl(find_cpl(dir));
3337
3338         check_verify_result(
3339                 { dir },
3340                 {
3341                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3342                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3343                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "width 1997 is not a multiple of 2", canonical(find_cpl(dir)) },
3344                         { 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)) },
3345                 });
3346 }
3347
3348
3349 BOOST_AUTO_TEST_CASE(verify_invalid_main_picture_active_area_2)
3350 {
3351         path dir = "build/test/verify_invalid_main_picture_active_area_2";
3352         prepare_directory(dir);
3353         auto dcp = make_simple(dir, 1, 24);
3354         dcp->write_xml();
3355
3356         auto constexpr area = "<meta:MainPictureActiveArea>";
3357
3358         {
3359                 Editor e(find_cpl(dir));
3360                 e.delete_lines_after(area, 2);
3361                 e.insert(area, "<meta:Height>5125</meta:Height>");
3362                 e.insert(area, "<meta:Width>9900</meta:Width>");
3363         }
3364
3365         dcp::PKL pkl(find_pkl(dir));
3366         dcp::CPL cpl(find_cpl(dir));
3367
3368         check_verify_result(
3369                 { dir },
3370                 {
3371                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::MISMATCHED_CPL_HASHES, cpl.id(), canonical(find_cpl(dir)) },
3372                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3373                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::INVALID_MAIN_PICTURE_ACTIVE_AREA, "height 5125 is not a multiple of 2", canonical(find_cpl(dir)) },
3374                         { 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)) },
3375                         { 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)) },
3376                 });
3377 }
3378
3379
3380 BOOST_AUTO_TEST_CASE(verify_duplicate_pkl_asset_ids)
3381 {
3382         RNGFixer rg;
3383
3384         path dir = "build/test/verify_duplicate_pkl_asset_ids";
3385         prepare_directory(dir);
3386         auto dcp = make_simple(dir, 1, 24);
3387         dcp->write_xml();
3388
3389         {
3390                 Editor e(find_pkl(dir));
3391                 e.replace("urn:uuid:5407b210-4441-4e97-8b16-8bdc7c12da54", "urn:uuid:6affb8ee-0020-4dff-a53c-17652f6358ab");
3392         }
3393
3394         dcp::PKL pkl(find_pkl(dir));
3395
3396         check_verify_result(
3397                 { dir },
3398                 {
3399                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_PKL, pkl.id(), canonical(find_pkl(dir)) },
3400                 });
3401 }
3402
3403
3404 BOOST_AUTO_TEST_CASE(verify_duplicate_assetmap_asset_ids)
3405 {
3406         RNGFixer rg;
3407
3408         path dir = "build/test/verify_duplicate_assetmap_asset_ids";
3409         prepare_directory(dir);
3410         auto dcp = make_simple(dir, 1, 24);
3411         dcp->write_xml();
3412
3413         {
3414                 Editor e(find_asset_map(dir));
3415                 e.replace("urn:uuid:5407b210-4441-4e97-8b16-8bdc7c12da54", "urn:uuid:97f0f352-5b77-48ee-a558-9df37717f4fa");
3416         }
3417
3418         dcp::PKL pkl(find_pkl(dir));
3419         dcp::AssetMap asset_map(find_asset_map(dir));
3420
3421         check_verify_result(
3422                 { dir },
3423                 {
3424                         { dcp::VerificationNote::Type::BV21_ERROR, dcp::VerificationNote::Code::MISMATCHED_PKL_ANNOTATION_TEXT_WITH_CPL, pkl.id(), canonical(find_pkl(dir)), },
3425                         { dcp::VerificationNote::Type::ERROR, dcp::VerificationNote::Code::DUPLICATE_ASSET_ID_IN_ASSETMAP, asset_map.id(), canonical(find_asset_map(dir)) },
3426                         { dcp::VerificationNote::Type::WARNING, dcp::VerificationNote::Code::EXTERNAL_ASSET, string("5407b210-4441-4e97-8b16-8bdc7c12da54") },
3427                 });
3428 }
3429