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