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