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