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