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