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