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