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