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