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