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