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